@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 { aPng: baseScreenshotPng, bPng: currentScreenshotPng, dimensions, } = await padToSameCanvas(baseScreenshotBuffer, currentScreenshotBuffer);
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
- const totalPixels = dimensions.width * dimensions.height;
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(PNG.sync.write(baseScreenshotPng), 'expected');
154
- await writeExpectationScreenshot(PNG.sync.write(currentScreenshotPng), 'actual');
155
- await writeExpectationScreenshot(PNG.sync.write(diffPng), 'diff');
156
- throw new Error(`Screenshot mismatch: ${screenshotFilePath}\n diff=${diffPixelCount}px (${(diffRatio * 100).toFixed(3)}%) (limit: ${(defaultScreenshotOptions.maxDiffPixelRatio * 100).toFixed(3)}%). Run with --update-snapshots to update screenshot.`);
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.61.0",
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.61.0",
48
- "@augment-vir/common": "^31.61.0",
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"