@augment-vir/test 31.61.0 → 31.63.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.
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Dimensions } from '@augment-vir/common';
|
|
2
|
+
import { PNG } from 'pngjs';
|
|
3
|
+
/**
|
|
4
|
+
* Options for image comparison thresholds.
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
*/
|
|
8
|
+
export type ImageComparisonOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* Per-pixel color threshold for `pixelmatch` (0–1). Smaller values make comparison more
|
|
11
|
+
* sensitive.
|
|
12
|
+
*/
|
|
13
|
+
threshold: number;
|
|
14
|
+
/** Maximum ratio of differing pixels allowed before the comparison is considered a failure. */
|
|
15
|
+
maxDiffPixelRatio: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Default image comparison options used in {@link compareImages}.
|
|
19
|
+
*
|
|
20
|
+
* @category Internal
|
|
21
|
+
*/
|
|
22
|
+
export declare const defaultImageComparisonOptions: Readonly<ImageComparisonOptions>;
|
|
23
|
+
/**
|
|
24
|
+
* The result of comparing two images via {@link compareImages}.
|
|
25
|
+
*
|
|
26
|
+
* @category Internal
|
|
27
|
+
*/
|
|
28
|
+
export type ImageComparisonResult = {
|
|
29
|
+
/** Whether the images match within the allowed diff ratio. */
|
|
30
|
+
passed: boolean;
|
|
31
|
+
/** The number of differing pixels. */
|
|
32
|
+
diffPixelCount: number;
|
|
33
|
+
/** Total pixel count of the (padded) comparison canvas. */
|
|
34
|
+
totalPixels: number;
|
|
35
|
+
/** The ratio of differing pixels to total pixels. */
|
|
36
|
+
diffRatio: number;
|
|
37
|
+
/** The dimensions of the comparison canvas. */
|
|
38
|
+
dimensions: Readonly<Dimensions>;
|
|
39
|
+
/** PNG data for the base image (padded to the comparison canvas). */
|
|
40
|
+
basePng: PNG;
|
|
41
|
+
/** PNG data for the current image (padded to the comparison canvas). */
|
|
42
|
+
currentPng: PNG;
|
|
43
|
+
/** PNG data for the visual diff output. */
|
|
44
|
+
diffPng: PNG;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Pads both images to the same canvas size (max width/height of the two) without scaling, then
|
|
48
|
+
* returns the decoded PNGs and the shared dimensions.
|
|
49
|
+
*
|
|
50
|
+
* @category Internal
|
|
51
|
+
*/
|
|
52
|
+
export declare function padToSameCanvas(aBuf: Buffer, bBuf: Buffer): Promise<{
|
|
53
|
+
aPng: import("pngjs").PNGWithMetadata;
|
|
54
|
+
bPng: import("pngjs").PNGWithMetadata;
|
|
55
|
+
dimensions: Readonly<Dimensions>;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Compare two PNG image buffers pixel-by-pixel. The images may have different dimensions — they
|
|
59
|
+
* will be padded to the same canvas size before comparison.
|
|
60
|
+
*
|
|
61
|
+
* This function is fully Playwright-agnostic and can be used in any Node.js context.
|
|
62
|
+
*
|
|
63
|
+
* @category Internal
|
|
64
|
+
*/
|
|
65
|
+
export declare function compareImages(baseImageBuffer: Buffer, currentImageBuffer: Buffer, options?: Readonly<ImageComparisonOptions>): Promise<ImageComparisonResult>;
|
|
66
|
+
/**
|
|
67
|
+
* Encode a {@link PNG} instance to a `Buffer`.
|
|
68
|
+
*
|
|
69
|
+
* @category Internal
|
|
70
|
+
*/
|
|
71
|
+
export declare function encodePng(png: PNG): Buffer;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import pixelmatch from 'pixelmatch';
|
|
2
|
+
import { PNG } from 'pngjs';
|
|
3
|
+
import sharp from 'sharp';
|
|
4
|
+
/**
|
|
5
|
+
* Default image comparison options used in {@link compareImages}.
|
|
6
|
+
*
|
|
7
|
+
* @category Internal
|
|
8
|
+
*/
|
|
9
|
+
export const defaultImageComparisonOptions = {
|
|
10
|
+
threshold: 0.1,
|
|
11
|
+
maxDiffPixelRatio: 0.08,
|
|
12
|
+
};
|
|
13
|
+
async function padImage(image, { height, width }) {
|
|
14
|
+
return await sharp({
|
|
15
|
+
create: { width, height, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
|
|
16
|
+
})
|
|
17
|
+
/** Top-left align. */
|
|
18
|
+
.composite([{ input: image, left: 0, top: 0 }])
|
|
19
|
+
.png()
|
|
20
|
+
.toBuffer();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Pads both images to the same canvas size (max width/height of the two) without scaling, then
|
|
24
|
+
* returns the decoded PNGs and the shared dimensions.
|
|
25
|
+
*
|
|
26
|
+
* @category Internal
|
|
27
|
+
*/
|
|
28
|
+
export async function padToSameCanvas(aBuf, bBuf) {
|
|
29
|
+
const [aMeta, bMeta,] = await Promise.all([
|
|
30
|
+
sharp(aBuf).metadata(),
|
|
31
|
+
sharp(bBuf).metadata(),
|
|
32
|
+
]);
|
|
33
|
+
if (!aMeta.width || !aMeta.height || !bMeta.width || !bMeta.height) {
|
|
34
|
+
throw new Error('Unable to read image dimensions.');
|
|
35
|
+
}
|
|
36
|
+
const dimensions = {
|
|
37
|
+
width: Math.max(aMeta.width, bMeta.width),
|
|
38
|
+
height: Math.max(aMeta.height, bMeta.height),
|
|
39
|
+
};
|
|
40
|
+
const [aPadded, bPadded,] = await Promise.all([
|
|
41
|
+
padImage(aBuf, dimensions),
|
|
42
|
+
padImage(bBuf, dimensions),
|
|
43
|
+
]);
|
|
44
|
+
const aPng = PNG.sync.read(aPadded);
|
|
45
|
+
const bPng = PNG.sync.read(bPadded);
|
|
46
|
+
return {
|
|
47
|
+
aPng,
|
|
48
|
+
bPng,
|
|
49
|
+
dimensions,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Compare two PNG image buffers pixel-by-pixel. The images may have different dimensions — they
|
|
54
|
+
* will be padded to the same canvas size before comparison.
|
|
55
|
+
*
|
|
56
|
+
* This function is fully Playwright-agnostic and can be used in any Node.js context.
|
|
57
|
+
*
|
|
58
|
+
* @category Internal
|
|
59
|
+
*/
|
|
60
|
+
export async function compareImages(baseImageBuffer, currentImageBuffer, options = defaultImageComparisonOptions) {
|
|
61
|
+
const { aPng: basePng, bPng: currentPng, dimensions, } = await padToSameCanvas(baseImageBuffer, currentImageBuffer);
|
|
62
|
+
const diffPng = new PNG(dimensions);
|
|
63
|
+
const diffPixelCount = pixelmatch(basePng.data, currentPng.data, diffPng.data, dimensions.width, dimensions.height, {
|
|
64
|
+
threshold: options.threshold,
|
|
65
|
+
});
|
|
66
|
+
const totalPixels = dimensions.width * dimensions.height;
|
|
67
|
+
const diffRatio = diffPixelCount / totalPixels;
|
|
68
|
+
const passed = diffRatio <= options.maxDiffPixelRatio;
|
|
69
|
+
return {
|
|
70
|
+
passed,
|
|
71
|
+
diffPixelCount,
|
|
72
|
+
totalPixels,
|
|
73
|
+
diffRatio,
|
|
74
|
+
dimensions,
|
|
75
|
+
basePng,
|
|
76
|
+
currentPng,
|
|
77
|
+
diffPng,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Encode a {@link PNG} instance to a `Buffer`.
|
|
82
|
+
*
|
|
83
|
+
* @category Internal
|
|
84
|
+
*/
|
|
85
|
+
export function encodePng(png) {
|
|
86
|
+
return PNG.sync.write(png);
|
|
87
|
+
}
|
|
@@ -5,10 +5,8 @@ import { expect } from '@playwright/test';
|
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import { readFile } from 'node:fs/promises';
|
|
7
7
|
import { relative } from 'node:path';
|
|
8
|
-
import pixelmatch from 'pixelmatch';
|
|
9
|
-
import { PNG } from 'pngjs';
|
|
10
|
-
import sharp from 'sharp';
|
|
11
8
|
import { assertTestContext, assertWrapTestContext, TestEnv, } from '../augments/universal-testing-suite/universal-test-context.js';
|
|
9
|
+
import { compareImages, defaultImageComparisonOptions, encodePng } from './compare-images.js';
|
|
12
10
|
/** This is used for type extraction because Playwright does not export the types we need. */
|
|
13
11
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
14
12
|
function extractScreenshotMethod() {
|
|
@@ -29,40 +27,6 @@ export const defaultScreenshotOptions = {
|
|
|
29
27
|
threshold: 0.1,
|
|
30
28
|
maxDiffPixelRatio: 0.08,
|
|
31
29
|
};
|
|
32
|
-
async function padImage(image, { height, width }) {
|
|
33
|
-
return await sharp({
|
|
34
|
-
create: { width, height, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
|
|
35
|
-
})
|
|
36
|
-
/** Top-left align. */
|
|
37
|
-
.composite([{ input: image, left: 0, top: 0 }])
|
|
38
|
-
.png()
|
|
39
|
-
.toBuffer();
|
|
40
|
-
}
|
|
41
|
-
/** Pads both images to the same canvas (max width/height) without scaling. */
|
|
42
|
-
async function padToSameCanvas(aBuf, bBuf) {
|
|
43
|
-
const [aMeta, bMeta,] = await Promise.all([
|
|
44
|
-
sharp(aBuf).metadata(),
|
|
45
|
-
sharp(bBuf).metadata(),
|
|
46
|
-
]);
|
|
47
|
-
if (!aMeta.width || !aMeta.height || !bMeta.width || !bMeta.height) {
|
|
48
|
-
throw new Error('Unable to read image dimensions.');
|
|
49
|
-
}
|
|
50
|
-
const dimensions = {
|
|
51
|
-
width: Math.max(aMeta.width, bMeta.width),
|
|
52
|
-
height: Math.max(aMeta.height, bMeta.height),
|
|
53
|
-
};
|
|
54
|
-
const [aPadded, bPadded,] = await Promise.all([
|
|
55
|
-
padImage(aBuf, dimensions),
|
|
56
|
-
padImage(bBuf, dimensions),
|
|
57
|
-
]);
|
|
58
|
-
const aPng = PNG.sync.read(aPadded);
|
|
59
|
-
const bPng = PNG.sync.read(bPadded);
|
|
60
|
-
return {
|
|
61
|
-
aPng,
|
|
62
|
-
bPng,
|
|
63
|
-
dimensions,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
30
|
async function takeScreenshotBuffer(testContext, options = {}) {
|
|
67
31
|
if (options.locator) {
|
|
68
32
|
/** The locator expectation has different options than the page expectation. */
|
|
@@ -137,23 +101,19 @@ export async function expectScreenshot(testContext, options) {
|
|
|
137
101
|
throw new Error(`Baseline screenshot not found: ${screenshotFilePath}. Re-run with --update-snapshots to create it.`);
|
|
138
102
|
}
|
|
139
103
|
const baseScreenshotBuffer = await readFile(screenshotFilePath);
|
|
140
|
-
const
|
|
141
|
-
const diffPng = new PNG(dimensions);
|
|
142
|
-
const diffPixelCount = pixelmatch(baseScreenshotPng.data, currentScreenshotPng.data, diffPng.data, dimensions.width, dimensions.height, {
|
|
104
|
+
const result = await compareImages(baseScreenshotBuffer, currentScreenshotBuffer, {
|
|
143
105
|
threshold: defaultScreenshotOptions.threshold,
|
|
106
|
+
maxDiffPixelRatio: defaultScreenshotOptions.maxDiffPixelRatio,
|
|
144
107
|
});
|
|
145
|
-
|
|
146
|
-
const diffRatio = diffPixelCount / totalPixels;
|
|
147
|
-
const ratioOk = diffRatio <= defaultScreenshotOptions.maxDiffPixelRatio;
|
|
148
|
-
if (!ratioOk) {
|
|
108
|
+
if (!result.passed) {
|
|
149
109
|
if (process.env.CI) {
|
|
150
110
|
await writeNewScreenshot();
|
|
151
111
|
}
|
|
152
112
|
else {
|
|
153
|
-
await writeExpectationScreenshot(
|
|
154
|
-
await writeExpectationScreenshot(
|
|
155
|
-
await writeExpectationScreenshot(
|
|
156
|
-
throw new Error(`Screenshot mismatch: ${screenshotFilePath}\n diff=${diffPixelCount}px (${(diffRatio * 100).toFixed(3)}%) (limit: ${(
|
|
113
|
+
await writeExpectationScreenshot(encodePng(result.basePng), 'expected');
|
|
114
|
+
await writeExpectationScreenshot(encodePng(result.currentPng), 'actual');
|
|
115
|
+
await writeExpectationScreenshot(encodePng(result.diffPng), 'diff');
|
|
116
|
+
throw new Error(`Screenshot mismatch: ${screenshotFilePath}\n diff=${result.diffPixelCount}px (${(result.diffRatio * 100).toFixed(3)}%) (limit: ${(defaultImageComparisonOptions.maxDiffPixelRatio * 100).toFixed(3)}%). Run with --update-snapshots to update screenshot.`);
|
|
157
117
|
}
|
|
158
118
|
}
|
|
159
119
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@augment-vir/test",
|
|
3
|
-
"version": "31.
|
|
3
|
+
"version": "31.63.0",
|
|
4
4
|
"description": "A universal testing suite that works with Mocha style test runners _and_ Node.js's built-in test runner.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"test",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"test:web": "virmator test --no-deps web 'src/test-web/**/*.test.ts' 'src/augments/universal-testing-suite/**/*.test.ts'"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@augment-vir/assert": "^31.
|
|
48
|
-
"@augment-vir/common": "^31.
|
|
47
|
+
"@augment-vir/assert": "^31.63.0",
|
|
48
|
+
"@augment-vir/common": "^31.63.0",
|
|
49
49
|
"@date-vir/duration": "^8.1.0",
|
|
50
50
|
"@virmator/test": "^14.5.1",
|
|
51
51
|
"type-fest": "^5.4.4"
|