gestament 0.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.
Files changed (68) hide show
  1. package/README.md +122 -0
  2. package/dist/element.d.ts +15 -0
  3. package/dist/element.d.ts.map +1 -0
  4. package/dist/errors.d.ts +28 -0
  5. package/dist/errors.d.ts.map +1 -0
  6. package/dist/generated/packageMetadata.d.ts +18 -0
  7. package/dist/generated/packageMetadata.d.ts.map +1 -0
  8. package/dist/gestament-config.cjs +87 -0
  9. package/dist/gestament-config.cjs.map +1 -0
  10. package/dist/gestament-config.d.ts +18 -0
  11. package/dist/gestament-config.d.ts.map +1 -0
  12. package/dist/gestament-config.mjs +86 -0
  13. package/dist/gestament-config.mjs.map +1 -0
  14. package/dist/gestament-tray-host.cjs +12 -0
  15. package/dist/gestament-tray-host.cjs.map +1 -0
  16. package/dist/gestament-tray-host.d.ts +13 -0
  17. package/dist/gestament-tray-host.d.ts.map +1 -0
  18. package/dist/gestament-tray-host.mjs +11 -0
  19. package/dist/gestament-tray-host.mjs.map +1 -0
  20. package/dist/gestament-xvfb-worker.cjs +138 -0
  21. package/dist/gestament-xvfb-worker.cjs.map +1 -0
  22. package/dist/gestament-xvfb-worker.d.ts +13 -0
  23. package/dist/gestament-xvfb-worker.d.ts.map +1 -0
  24. package/dist/gestament-xvfb-worker.mjs +137 -0
  25. package/dist/gestament-xvfb-worker.mjs.map +1 -0
  26. package/dist/gestament-xvfb.cjs +132 -0
  27. package/dist/gestament-xvfb.cjs.map +1 -0
  28. package/dist/gestament-xvfb.d.ts +13 -0
  29. package/dist/gestament-xvfb.d.ts.map +1 -0
  30. package/dist/gestament-xvfb.mjs +131 -0
  31. package/dist/gestament-xvfb.mjs.map +1 -0
  32. package/dist/index.cjs +1077 -0
  33. package/dist/index.cjs.map +1 -0
  34. package/dist/index.d.ts +13 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.mjs +1077 -0
  37. package/dist/index.mjs.map +1 -0
  38. package/dist/launchGtkApp.d.ts +37 -0
  39. package/dist/launchGtkApp.d.ts.map +1 -0
  40. package/dist/native-BRnrsqMn.cjs +249 -0
  41. package/dist/native-BRnrsqMn.cjs.map +1 -0
  42. package/dist/native-DAhTiLnf.js +249 -0
  43. package/dist/native-DAhTiLnf.js.map +1 -0
  44. package/dist/native.d.ts +170 -0
  45. package/dist/native.d.ts.map +1 -0
  46. package/dist/testing.cjs +1180 -0
  47. package/dist/testing.cjs.map +1 -0
  48. package/dist/testing.d.ts +329 -0
  49. package/dist/testing.d.ts.map +1 -0
  50. package/dist/testing.mjs +1158 -0
  51. package/dist/testing.mjs.map +1 -0
  52. package/dist/tray.d.ts +17 -0
  53. package/dist/tray.d.ts.map +1 -0
  54. package/dist/types.d.ts +920 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/images/gestament-120.png +0 -0
  57. package/include/gestament/gtk.h +112 -0
  58. package/package.json +92 -0
  59. package/prebuilds/linux-arm/gtk3/node.napi.armv7.glibc.node +0 -0
  60. package/prebuilds/linux-arm/gtk4/node.napi.armv7.glibc.node +0 -0
  61. package/prebuilds/linux-arm64/gtk3/node.napi.glibc.node +0 -0
  62. package/prebuilds/linux-arm64/gtk4/node.napi.glibc.node +0 -0
  63. package/prebuilds/linux-ia32/gtk3/node.napi.glibc.node +0 -0
  64. package/prebuilds/linux-ia32/gtk4/node.napi.glibc.node +0 -0
  65. package/prebuilds/linux-riscv64/gtk3/node.napi.glibc.node +0 -0
  66. package/prebuilds/linux-riscv64/gtk4/node.napi.glibc.node +0 -0
  67. package/prebuilds/linux-x64/gtk3/node.napi.glibc.node +0 -0
  68. package/prebuilds/linux-x64/gtk4/node.napi.glibc.node +0 -0
@@ -0,0 +1,1158 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { PNG } from "pngjs";
6
+ import { ssim } from "ssim.js";
7
+ import __screwUpDefaultImportModule1 from "@tesseract.js-data/eng";
8
+ function pixelmatch$1(img1, img2, output, width, height, options = {}) {
9
+ const {
10
+ threshold = 0.1,
11
+ alpha = 0.1,
12
+ aaColor = [255, 255, 0],
13
+ diffColor = [255, 0, 0],
14
+ checkerboard = true,
15
+ includeAA,
16
+ diffColorAlt,
17
+ diffMask
18
+ } = options;
19
+ if (!isPixelData(img1) || !isPixelData(img2) || output && !isPixelData(output))
20
+ throw new Error("Image data: Uint8Array, Uint8ClampedArray or Buffer expected.");
21
+ if (img1.length !== img2.length || output && output.length !== img1.length)
22
+ throw new Error(`Image sizes do not match. Image 1 size: ${img1.length}, image 2 size: ${img2.length}`);
23
+ if (img1.length !== width * height * 4) throw new Error(`Image data size does not match width/height. Expecting ${width * height * 4}. Got ${img1.length}`);
24
+ const len = width * height;
25
+ const a32 = new Uint32Array(img1.buffer, img1.byteOffset, len);
26
+ const b32 = new Uint32Array(img2.buffer, img2.byteOffset, len);
27
+ let identical = true;
28
+ for (let i = 0; i < len; i++) {
29
+ if (a32[i] !== b32[i]) {
30
+ identical = false;
31
+ break;
32
+ }
33
+ }
34
+ if (identical) {
35
+ if (output && !diffMask) {
36
+ for (let i = 0, pos = 0; i < len; i++, pos += 4) drawGrayPixel(img1, pos, alpha, output);
37
+ }
38
+ return 0;
39
+ }
40
+ const maxDelta = 35215 * threshold * threshold;
41
+ const [aaR, aaG, aaB] = aaColor;
42
+ const [diffR, diffG, diffB] = diffColor;
43
+ const [altR, altG, altB] = diffColorAlt || diffColor;
44
+ let diff = 0;
45
+ for (let i = 0, pos = 0; i < len; i++, pos += 4) {
46
+ const delta = a32[i] === b32[i] ? 0 : colorDelta(img1, img2, pos, pos, checkerboard);
47
+ if (Math.abs(delta) > maxDelta) {
48
+ const x = i % width;
49
+ const y = i / width | 0;
50
+ const isExcludedAA = !includeAA && (antialiased(img1, x, y, width, height, a32, b32, checkerboard) || antialiased(img2, x, y, width, height, b32, a32, checkerboard));
51
+ if (isExcludedAA) {
52
+ if (output && !diffMask) drawPixel(output, pos, aaR, aaG, aaB);
53
+ } else {
54
+ if (output) {
55
+ if (delta < 0) {
56
+ drawPixel(output, pos, altR, altG, altB);
57
+ } else {
58
+ drawPixel(output, pos, diffR, diffG, diffB);
59
+ }
60
+ }
61
+ diff++;
62
+ }
63
+ } else if (output && !diffMask) {
64
+ drawGrayPixel(img1, pos, alpha, output);
65
+ }
66
+ }
67
+ return diff;
68
+ }
69
+ function isPixelData(arr) {
70
+ return ArrayBuffer.isView(arr) && arr.BYTES_PER_ELEMENT === 1;
71
+ }
72
+ function antialiased(img, x1, y1, width, height, a32, b32, checkerboard) {
73
+ const x0 = Math.max(x1 - 1, 0);
74
+ const y0 = Math.max(y1 - 1, 0);
75
+ const x2 = Math.min(x1 + 1, width - 1);
76
+ const y2 = Math.min(y1 + 1, height - 1);
77
+ const pos4 = (y1 * width + x1) * 4;
78
+ const cr = img[pos4];
79
+ const cg = img[pos4 + 1];
80
+ const cb = img[pos4 + 2];
81
+ const ca = img[pos4 + 3];
82
+ let zeroes = x1 === x0 || x1 === x2 || y1 === y0 || y1 === y2 ? 1 : 0;
83
+ let min = 0;
84
+ let max = 0;
85
+ let minX = 0;
86
+ let minY = 0;
87
+ let maxX = 0;
88
+ let maxY = 0;
89
+ for (let x = x0; x <= x2; x++) {
90
+ for (let y = y0; y <= y2; y++) {
91
+ if (x === x1 && y === y1) continue;
92
+ const delta = brightnessDelta(img, pos4, (y * width + x) * 4, cr, cg, cb, ca, checkerboard);
93
+ if (delta === 0) {
94
+ zeroes++;
95
+ if (zeroes > 2) return false;
96
+ } else if (delta < min) {
97
+ min = delta;
98
+ minX = x;
99
+ minY = y;
100
+ } else if (delta > max) {
101
+ max = delta;
102
+ maxX = x;
103
+ maxY = y;
104
+ }
105
+ }
106
+ }
107
+ if (min === 0 || max === 0) return false;
108
+ return hasManySiblings(a32, minX, minY, width, height) && hasManySiblings(b32, minX, minY, width, height) || hasManySiblings(a32, maxX, maxY, width, height) && hasManySiblings(b32, maxX, maxY, width, height);
109
+ }
110
+ function hasManySiblings(img, x1, y1, width, height) {
111
+ const x0 = Math.max(x1 - 1, 0);
112
+ const y0 = Math.max(y1 - 1, 0);
113
+ const x2 = Math.min(x1 + 1, width - 1);
114
+ const y2 = Math.min(y1 + 1, height - 1);
115
+ const val = img[y1 * width + x1];
116
+ let zeroes = x1 === x0 || x1 === x2 || y1 === y0 || y1 === y2 ? 1 : 0;
117
+ for (let x = x0; x <= x2; x++) {
118
+ for (let y = y0; y <= y2; y++) {
119
+ if (x === x1 && y === y1) continue;
120
+ zeroes += +(val === img[y * width + x]);
121
+ if (zeroes > 2) return true;
122
+ }
123
+ }
124
+ return false;
125
+ }
126
+ function colorDelta(img1, img2, k, m, checkerboard) {
127
+ const r1 = img1[k];
128
+ const g1 = img1[k + 1];
129
+ const b1 = img1[k + 2];
130
+ const a1 = img1[k + 3];
131
+ const r2 = img2[m];
132
+ const g2 = img2[m + 1];
133
+ const b2 = img2[m + 2];
134
+ const a2 = img2[m + 3];
135
+ let dr = r1 - r2;
136
+ let dg = g1 - g2;
137
+ let db = b1 - b2;
138
+ const da = a1 - a2;
139
+ if (a1 < 255 || a2 < 255) {
140
+ let rb = 255, gb = 255, bb = 255;
141
+ if (checkerboard) {
142
+ rb = 48 + 159 * (k % 2);
143
+ gb = 48 + 159 * ((k / 1.618033988749895 | 0) % 2);
144
+ bb = 48 + 159 * ((k / 2.618033988749895 | 0) % 2);
145
+ }
146
+ dr = (r1 * a1 - r2 * a2 - rb * da) / 255;
147
+ dg = (g1 * a1 - g2 * a2 - gb * da) / 255;
148
+ db = (b1 * a1 - b2 * a2 - bb * da) / 255;
149
+ }
150
+ const y = dr * 0.29889531 + dg * 0.58662247 + db * 0.11448223;
151
+ const i = dr * 0.59597799 - dg * 0.2741761 - db * 0.32180189;
152
+ const q = dr * 0.21147017 - dg * 0.52261711 + db * 0.31114694;
153
+ const delta = 0.5053 * y * y + 0.299 * i * i + 0.1957 * q * q;
154
+ return y > 0 ? -delta : delta;
155
+ }
156
+ function brightnessDelta(img, k, m, r1, g1, b1, a1, checkerboard) {
157
+ const r2 = img[m];
158
+ const g2 = img[m + 1];
159
+ const b2 = img[m + 2];
160
+ const a2 = img[m + 3];
161
+ let dr = r1 - r2;
162
+ let dg = g1 - g2;
163
+ let db = b1 - b2;
164
+ const da = a1 - a2;
165
+ if (!dr && !dg && !db && !da) return 0;
166
+ if (a1 < 255 || a2 < 255) {
167
+ let rb = 255, gb = 255, bb = 255;
168
+ if (checkerboard) {
169
+ rb = 48 + 159 * (k % 2);
170
+ gb = 48 + 159 * ((k / 1.618033988749895 | 0) % 2);
171
+ bb = 48 + 159 * ((k / 2.618033988749895 | 0) % 2);
172
+ }
173
+ dr = (r1 * a1 - r2 * a2 - rb * da) / 255;
174
+ dg = (g1 * a1 - g2 * a2 - gb * da) / 255;
175
+ db = (b1 * a1 - b2 * a2 - bb * da) / 255;
176
+ }
177
+ return dr * 0.29889531 + dg * 0.58662247 + db * 0.11448223;
178
+ }
179
+ function drawPixel(output, pos, r, g, b) {
180
+ output[pos] = r;
181
+ output[pos + 1] = g;
182
+ output[pos + 2] = b;
183
+ output[pos + 3] = 255;
184
+ }
185
+ function drawGrayPixel(img, i, alpha, output) {
186
+ const val = 255 + (img[i] * 0.29889531 + img[i + 1] * 0.58662247 + img[i + 2] * 0.11448223 - 255) * alpha * img[i + 3] / 255;
187
+ drawPixel(output, i, val, val, val);
188
+ }
189
+ const __screwUpDefaultImportModule0 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
190
+ __proto__: null,
191
+ default: pixelmatch$1
192
+ }, Symbol.toStringTag, { value: "Module" }));
193
+ const pixelmatch = __resolveDefaultExport(__screwUpDefaultImportModule0, true);
194
+ const englishLanguageData = __resolveDefaultExport(__screwUpDefaultImportModule1, false);
195
+ globalThis.__screwUpIsInCJS_249951a7145e = false;
196
+ function __resolveDefaultExport(module, isESM) {
197
+ const __isInCJS = typeof globalThis !== "undefined" && globalThis.__screwUpIsInCJS_249951a7145e === true;
198
+ const maybe = module;
199
+ const hasDefault = !!(maybe && typeof maybe === "object" && "default" in maybe);
200
+ const unwrapNamespaceDefault = (value) => {
201
+ if (!value || typeof value !== "object") {
202
+ return value;
203
+ }
204
+ const inner = value;
205
+ const isModule = inner.__esModule === true || typeof Symbol !== "undefined" && inner[Symbol.toStringTag] === "Module";
206
+ if (isModule && "default" in inner) {
207
+ return inner.default;
208
+ }
209
+ return value;
210
+ };
211
+ const resolvedDefault = hasDefault ? unwrapNamespaceDefault(maybe.default) : void 0;
212
+ if (__isInCJS) {
213
+ return hasDefault ? resolvedDefault ?? module : module;
214
+ }
215
+ if (isESM) {
216
+ if (hasDefault) {
217
+ return resolvedDefault;
218
+ }
219
+ throw new Error("Default export not found.");
220
+ }
221
+ return hasDefault ? resolvedDefault ?? module : module;
222
+ }
223
+ const padNumber = (value, width) => value.toString().padStart(width, "0");
224
+ const hashText = (value) => createHash("sha256").update(value).digest("hex").slice(0, 10);
225
+ const defaultOcrPageSegmentationModes = [
226
+ "singleBlock",
227
+ "sparseText",
228
+ "singleLine",
229
+ "singleWord"
230
+ ];
231
+ const pageSegmentationModeValues = {
232
+ auto: "3",
233
+ autoOnly: "2",
234
+ autoOsd: "1",
235
+ circleWord: "9",
236
+ osdOnly: "0",
237
+ rawLine: "13",
238
+ singleBlock: "6",
239
+ singleBlockVerticalText: "5",
240
+ singleChar: "10",
241
+ singleColumn: "4",
242
+ singleLine: "7",
243
+ singleWord: "8",
244
+ sparseText: "11",
245
+ sparseTextOsd: "12"
246
+ };
247
+ let tesseractModulePromise;
248
+ const loadTesseractModule = async () => {
249
+ tesseractModulePromise ??= import("tesseract.js").then((loaded) => {
250
+ const module = loaded;
251
+ return "default" in module ? module.default : module;
252
+ });
253
+ return tesseractModulePromise;
254
+ };
255
+ const normalizePathSegment = (value, fallback) => {
256
+ const trimmed = value.trim();
257
+ const normalized = trimmed.replace(/[\x00-\x1f<>:"/\\|?*]+/g, "_").replace(/\s+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
258
+ if (normalized.length === 0) {
259
+ return fallback;
260
+ }
261
+ if (normalized.length <= 140) {
262
+ return normalized;
263
+ }
264
+ return `${normalized.slice(0, 129)}-${hashText(normalized)}`;
265
+ };
266
+ const serializeBounds = (bounds) => ({
267
+ height: bounds.height,
268
+ width: bounds.width,
269
+ x: bounds.x,
270
+ y: bounds.y
271
+ });
272
+ const resolveDefaults = (defaults, options) => {
273
+ const outputResultPath = options?.outputResultPath ?? defaults.outputResultPath ?? process.env.GESTAMENT_VISUAL_OUTPUT_RESULT_PATH;
274
+ if (outputResultPath === void 0) {
275
+ return void 0;
276
+ }
277
+ return {
278
+ outputResultPath: resolve(outputResultPath),
279
+ variant: options?.variant ?? defaults.variant ?? process.env.GESTAMENT_VISUAL_VARIANT ?? process.env.GESTAMENT_TEST_BACKEND ?? "default"
280
+ };
281
+ };
282
+ const loadExpectedImage = async (expectedImage) => {
283
+ if (Buffer.isBuffer(expectedImage)) {
284
+ return {
285
+ data: expectedImage,
286
+ source: "buffer",
287
+ sourcePath: void 0
288
+ };
289
+ }
290
+ if (typeof expectedImage === "string") {
291
+ const sourcePath2 = resolve(expectedImage);
292
+ return {
293
+ data: await readFile(sourcePath2),
294
+ source: "path",
295
+ sourcePath: sourcePath2
296
+ };
297
+ }
298
+ if (expectedImage.protocol !== "file:") {
299
+ throw new TypeError("expectedImage URL must use the file: protocol.");
300
+ }
301
+ const sourcePath = fileURLToPath(expectedImage);
302
+ return {
303
+ data: await readFile(sourcePath),
304
+ source: "file-url",
305
+ sourcePath
306
+ };
307
+ };
308
+ const decodePng = (image) => {
309
+ const png = PNG.sync.read(image);
310
+ return {
311
+ data: png.data,
312
+ height: png.height,
313
+ width: png.width
314
+ };
315
+ };
316
+ const validateCaptureImage = (capture, png) => {
317
+ if (png.width !== capture.visibleBounds.width || png.height !== capture.visibleBounds.height) {
318
+ throw new Error(
319
+ `Capture PNG size ${png.width}x${png.height} does not match visible bounds ${capture.visibleBounds.width}x${capture.visibleBounds.height}.`
320
+ );
321
+ }
322
+ };
323
+ const validateFiniteInteger = (value, label) => {
324
+ if (!Number.isInteger(value)) {
325
+ throw new TypeError(`${label} must be an integer.`);
326
+ }
327
+ };
328
+ const validateRegion = (region, imageWidth, imageHeight, label) => {
329
+ validateFiniteInteger(region.x, `${label}.x`);
330
+ validateFiniteInteger(region.y, `${label}.y`);
331
+ validateFiniteInteger(region.width, `${label}.width`);
332
+ validateFiniteInteger(region.height, `${label}.height`);
333
+ if (region.x < 0 || region.y < 0) {
334
+ throw new TypeError(`${label} origin must be inside the capture image.`);
335
+ }
336
+ if (region.width <= 0 || region.height <= 0) {
337
+ throw new TypeError(`${label} size must be positive.`);
338
+ }
339
+ if (region.x + region.width > imageWidth || region.y + region.height > imageHeight) {
340
+ throw new TypeError(`${label} must be inside the capture image.`);
341
+ }
342
+ };
343
+ const validateRatio = (value, label) => {
344
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
345
+ throw new TypeError(`${label} must be a number from 0 to 1.`);
346
+ }
347
+ };
348
+ const validateNonNegativeInteger = (value, label) => {
349
+ if (!Number.isInteger(value) || value < 0) {
350
+ throw new TypeError(`${label} must be a non-negative integer.`);
351
+ }
352
+ };
353
+ const getComparisonRegion = (options, imageWidth, imageHeight) => {
354
+ const region = options?.region ?? { height: imageHeight, width: imageWidth, x: 0, y: 0 };
355
+ validateRegion(region, imageWidth, imageHeight, "region");
356
+ return region;
357
+ };
358
+ const validateMasks = (masks, imageWidth, imageHeight) => {
359
+ if (masks === void 0) {
360
+ return;
361
+ }
362
+ for (const [index, mask] of masks.entries()) {
363
+ validateRegion(mask, imageWidth, imageHeight, `masks[${index}]`);
364
+ }
365
+ };
366
+ const copyRegionData = (source, region) => {
367
+ const data = new Uint8Array(region.width * region.height * 4);
368
+ for (let y = 0; y < region.height; y += 1) {
369
+ const sourceStart = ((region.y + y) * source.width + region.x) * 4;
370
+ const sourceEnd = sourceStart + region.width * 4;
371
+ const targetStart = y * region.width * 4;
372
+ data.set(source.data.subarray(sourceStart, sourceEnd), targetStart);
373
+ }
374
+ return data;
375
+ };
376
+ const applyMasks = (actualData, expectedData, region, masks) => {
377
+ if (masks === void 0) {
378
+ return;
379
+ }
380
+ for (const mask of masks) {
381
+ const left = Math.max(region.x, mask.x);
382
+ const top = Math.max(region.y, mask.y);
383
+ const right = Math.min(region.x + region.width, mask.x + mask.width);
384
+ const bottom = Math.min(region.y + region.height, mask.y + mask.height);
385
+ if (left >= right || top >= bottom) {
386
+ continue;
387
+ }
388
+ for (let y = top; y < bottom; y += 1) {
389
+ for (let x = left; x < right; x += 1) {
390
+ const index = ((y - region.y) * region.width + (x - region.x)) * 4;
391
+ actualData[index] = 0;
392
+ actualData[index + 1] = 0;
393
+ actualData[index + 2] = 0;
394
+ actualData[index + 3] = 0;
395
+ expectedData[index] = 0;
396
+ expectedData[index + 1] = 0;
397
+ expectedData[index + 2] = 0;
398
+ expectedData[index + 3] = 0;
399
+ }
400
+ }
401
+ }
402
+ };
403
+ const prepareComparison = (actualPng, expectedPng, options) => {
404
+ if (actualPng.width !== expectedPng.width || actualPng.height !== expectedPng.height) {
405
+ throw new Error(
406
+ `Expected image size ${expectedPng.width}x${expectedPng.height} does not match actual capture size ${actualPng.width}x${actualPng.height}.`
407
+ );
408
+ }
409
+ const region = getComparisonRegion(
410
+ options,
411
+ actualPng.width,
412
+ actualPng.height
413
+ );
414
+ validateMasks(options?.masks, actualPng.width, actualPng.height);
415
+ const actualData = copyRegionData(actualPng, region);
416
+ const expectedData = copyRegionData(expectedPng, region);
417
+ applyMasks(actualData, expectedData, region, options?.masks);
418
+ return {
419
+ actualData,
420
+ expectedData,
421
+ height: region.height,
422
+ totalPixels: region.width * region.height,
423
+ width: region.width
424
+ };
425
+ };
426
+ const createDiffPng = (comparison, threshold) => {
427
+ const diffPng = new PNG({
428
+ height: comparison.height,
429
+ width: comparison.width
430
+ });
431
+ const diffPixels = pixelmatch(
432
+ comparison.expectedData,
433
+ comparison.actualData,
434
+ diffPng.data,
435
+ comparison.width,
436
+ comparison.height,
437
+ { threshold }
438
+ );
439
+ return {
440
+ diffPixels,
441
+ diffPng
442
+ };
443
+ };
444
+ const createContext = async (defaults, options, name, counter) => {
445
+ const resolved = resolveDefaults(defaults, options);
446
+ if (resolved === void 0) {
447
+ return {
448
+ artifactsEnabled: false
449
+ };
450
+ }
451
+ const safeVariant = normalizePathSegment(resolved.variant, "default");
452
+ const safeName = normalizePathSegment(name, "capture");
453
+ const outputResultPath = join(
454
+ resolved.outputResultPath,
455
+ safeVariant,
456
+ `${safeName}-${padNumber(counter, 6)}`
457
+ );
458
+ await mkdir(outputResultPath, { recursive: true });
459
+ return {
460
+ actualImagePath: join(outputResultPath, "actual.png"),
461
+ outputResultPath,
462
+ artifactsEnabled: true,
463
+ diffImagePath: join(outputResultPath, "diff.png"),
464
+ expectedImagePath: join(outputResultPath, "expected.png"),
465
+ metadataJsonPath: join(outputResultPath, "metadata.json"),
466
+ ocrInputImagePath: join(outputResultPath, "ocr-input.png"),
467
+ resolved
468
+ };
469
+ };
470
+ const writeMetadata = async (context, capture, name, matcher, options, expectedImage) => {
471
+ await writeFile(
472
+ context.metadataJsonPath,
473
+ `${JSON.stringify(
474
+ {
475
+ bounds: serializeBounds(capture.bounds),
476
+ clipped: capture.clipped,
477
+ expectedImageSource: expectedImage.source,
478
+ expectedImageSourcePath: expectedImage.sourcePath,
479
+ imageBytes: capture.image.length,
480
+ matcher,
481
+ masks: options?.masks,
482
+ name,
483
+ region: options?.region,
484
+ variant: context.resolved.variant,
485
+ visibleBounds: serializeBounds(capture.visibleBounds)
486
+ },
487
+ void 0,
488
+ 2
489
+ )}
490
+ `
491
+ );
492
+ };
493
+ const createVisualError = (message, result) => Object.assign(new Error(message), {
494
+ result
495
+ });
496
+ const saveActualAndMetadata = async (context, capture, name, matcher, options, expectedImage) => {
497
+ if (!context.artifactsEnabled) {
498
+ return;
499
+ }
500
+ await writeFile(context.actualImagePath, capture.image);
501
+ await writeMetadata(context, capture, name, matcher, options, expectedImage);
502
+ };
503
+ const buildBaseResult = (context) => {
504
+ if (!context.artifactsEnabled) {
505
+ return {
506
+ pass: true
507
+ };
508
+ }
509
+ return {
510
+ actualImagePath: context.actualImagePath,
511
+ outputResultPath: context.outputResultPath,
512
+ metadataJsonPath: context.metadataJsonPath,
513
+ pass: true
514
+ };
515
+ };
516
+ const writeFailureArtifacts = async (context, expectedImage, diffPng) => {
517
+ if (!context.artifactsEnabled) {
518
+ return {};
519
+ }
520
+ await writeFile(context.expectedImagePath, expectedImage);
521
+ await writeFile(context.diffImagePath, PNG.sync.write(diffPng));
522
+ return {
523
+ diffImagePath: context.diffImagePath,
524
+ expectedImagePath: context.expectedImagePath
525
+ };
526
+ };
527
+ const isLookSimilarPass = (diffPixels, diffRatio, options) => {
528
+ const ratioPass = diffRatio <= options.maxDiffRatio;
529
+ const pixelsPass = options.maxDiffPixels === void 0 || diffPixels <= options.maxDiffPixels;
530
+ return ratioPass && pixelsPass;
531
+ };
532
+ const resolveLookSimilarOptions = (options) => {
533
+ const resolved = {
534
+ maxDiffPixels: options?.maxDiffPixels,
535
+ maxDiffRatio: options?.maxDiffRatio ?? 0.01,
536
+ threshold: options?.threshold ?? 0.1
537
+ };
538
+ validateRatio(resolved.threshold, "threshold");
539
+ validateRatio(resolved.maxDiffRatio, "maxDiffRatio");
540
+ if (resolved.maxDiffPixels !== void 0) {
541
+ validateNonNegativeInteger(resolved.maxDiffPixels, "maxDiffPixels");
542
+ }
543
+ return resolved;
544
+ };
545
+ const resolveSimilarityOptions = (options) => {
546
+ const resolved = {
547
+ minSimilarity: options?.minSimilarity ?? 0.985
548
+ };
549
+ validateRatio(resolved.minSimilarity, "minSimilarity");
550
+ return resolved;
551
+ };
552
+ const validateOcrPageSegmentationMode = (value, label) => {
553
+ if (!(value in pageSegmentationModeValues)) {
554
+ throw new TypeError(
555
+ `${label} must be a supported OCR page segmentation mode.`
556
+ );
557
+ }
558
+ };
559
+ const validateThreshold = (value, label) => {
560
+ if (!Number.isInteger(value) || value < 0 || value > 255) {
561
+ throw new TypeError(`${label} must be an integer from 0 to 255.`);
562
+ }
563
+ };
564
+ const validatePositiveInteger = (value, label) => {
565
+ if (!Number.isInteger(value) || value <= 0) {
566
+ throw new TypeError(`${label} must be a positive integer.`);
567
+ }
568
+ };
569
+ const normalizeWhitespace = (value) => value.replace(/\s+/g, " ").trim();
570
+ const resolveOcrPreprocessOptions = (options) => {
571
+ const resolved = {
572
+ grayscale: options?.grayscale ?? false,
573
+ invert: options?.invert ?? false,
574
+ scale: options?.scale ?? 1,
575
+ threshold: options?.threshold ?? -1
576
+ };
577
+ validatePositiveInteger(resolved.scale, "preprocess.scale");
578
+ if (resolved.threshold !== -1) {
579
+ validateThreshold(resolved.threshold, "preprocess.threshold");
580
+ }
581
+ return resolved;
582
+ };
583
+ const resolveOcrOptions = (options, imageWidth, imageHeight) => {
584
+ const pageSegmentationModes = options?.pageSegmentationModes ?? defaultOcrPageSegmentationModes;
585
+ if (pageSegmentationModes.length === 0) {
586
+ throw new TypeError("pageSegmentationModes must not be empty.");
587
+ }
588
+ for (const [index, mode] of pageSegmentationModes.entries()) {
589
+ validateOcrPageSegmentationMode(mode, `pageSegmentationModes[${index}]`);
590
+ }
591
+ const region = options?.region ?? { height: imageHeight, width: imageWidth, x: 0, y: 0 };
592
+ validateRegion(region, imageWidth, imageHeight, "region");
593
+ return {
594
+ pageSegmentationModes,
595
+ parameters: options?.parameters ?? {},
596
+ preprocess: resolveOcrPreprocessOptions(options?.preprocess),
597
+ region
598
+ };
599
+ };
600
+ const hasOcrPreprocessing = (options, region, imageWidth, imageHeight) => options.grayscale || options.invert || options.scale !== 1 || options.threshold !== -1 || region.x !== 0 || region.y !== 0 || region.width !== imageWidth || region.height !== imageHeight;
601
+ const getLuminance = (red, green, blue) => Math.round(red * 0.2126 + green * 0.7152 + blue * 0.0722);
602
+ const prepareOcrImage = (originalImage, png, options) => {
603
+ if (!hasOcrPreprocessing(
604
+ options.preprocess,
605
+ options.region,
606
+ png.width,
607
+ png.height
608
+ )) {
609
+ return {
610
+ image: originalImage,
611
+ preprocess: options.preprocess,
612
+ region: options.region
613
+ };
614
+ }
615
+ const output = new PNG({
616
+ height: options.region.height * options.preprocess.scale,
617
+ width: options.region.width * options.preprocess.scale
618
+ });
619
+ for (let y = 0; y < output.height; y += 1) {
620
+ for (let x = 0; x < output.width; x += 1) {
621
+ const sourceX = options.region.x + Math.floor(x / options.preprocess.scale);
622
+ const sourceY = options.region.y + Math.floor(y / options.preprocess.scale);
623
+ const sourceIndex = (sourceY * png.width + sourceX) * 4;
624
+ const targetIndex = (y * output.width + x) * 4;
625
+ const red = png.data[sourceIndex];
626
+ const green = png.data[sourceIndex + 1];
627
+ const blue = png.data[sourceIndex + 2];
628
+ const alpha = png.data[sourceIndex + 3];
629
+ const luminance = getLuminance(red, green, blue);
630
+ const thresholded = options.preprocess.threshold === -1 ? luminance : luminance >= options.preprocess.threshold ? 255 : 0;
631
+ const value = options.preprocess.grayscale || options.preprocess.threshold !== -1 ? thresholded : void 0;
632
+ output.data[targetIndex] = options.preprocess.invert ? 255 - (value ?? red) : value ?? red;
633
+ output.data[targetIndex + 1] = options.preprocess.invert ? 255 - (value ?? green) : value ?? green;
634
+ output.data[targetIndex + 2] = options.preprocess.invert ? 255 - (value ?? blue) : value ?? blue;
635
+ output.data[targetIndex + 3] = alpha;
636
+ }
637
+ }
638
+ return {
639
+ image: PNG.sync.write(output),
640
+ preprocess: options.preprocess,
641
+ region: options.region
642
+ };
643
+ };
644
+ const isEnglishOnlyLanguage = (languages) => {
645
+ if (languages === void 0) {
646
+ return true;
647
+ }
648
+ if (typeof languages === "string") {
649
+ return languages === "eng";
650
+ }
651
+ return languages.length === 1 && languages[0] === "eng";
652
+ };
653
+ const resolveOcrWorkerOptions = (defaults) => {
654
+ const languages = defaults?.languages === void 0 ? englishLanguageData.code : typeof defaults.languages === "string" ? defaults.languages : [...defaults.languages];
655
+ const useBundledEnglishData = defaults?.langPath === void 0 && isEnglishOnlyLanguage(languages);
656
+ const workerOptions = {
657
+ logger: () => void 0
658
+ };
659
+ if (defaults?.cachePath !== void 0) {
660
+ workerOptions.cachePath = defaults.cachePath;
661
+ }
662
+ if (defaults?.corePath !== void 0) {
663
+ workerOptions.corePath = defaults.corePath;
664
+ }
665
+ if (defaults?.workerPath !== void 0) {
666
+ workerOptions.workerPath = defaults.workerPath;
667
+ }
668
+ if (defaults?.langPath !== void 0) {
669
+ workerOptions.langPath = defaults.langPath;
670
+ } else if (useBundledEnglishData) {
671
+ workerOptions.langPath = englishLanguageData.langPath;
672
+ }
673
+ if (defaults?.gzip !== void 0) {
674
+ workerOptions.gzip = defaults.gzip;
675
+ } else if (useBundledEnglishData) {
676
+ workerOptions.gzip = englishLanguageData.gzip;
677
+ }
678
+ if (defaults?.cacheMethod !== void 0) {
679
+ workerOptions.cacheMethod = defaults.cacheMethod;
680
+ } else if (useBundledEnglishData) {
681
+ workerOptions.cacheMethod = "none";
682
+ }
683
+ return {
684
+ languages,
685
+ options: workerOptions
686
+ };
687
+ };
688
+ const createOcrWorker = async (defaults) => {
689
+ const tesseract = await loadTesseractModule();
690
+ const resolved = resolveOcrWorkerOptions(defaults);
691
+ return await tesseract.createWorker(
692
+ resolved.languages,
693
+ void 0,
694
+ resolved.options
695
+ );
696
+ };
697
+ const recognizeWithWorker = async (worker, preparedImage, options) => {
698
+ const attempts = [];
699
+ for (const pageSegmentationMode of options.pageSegmentationModes) {
700
+ await worker.setParameters({
701
+ ...options.parameters,
702
+ tessedit_pageseg_mode: pageSegmentationModeValues[pageSegmentationMode]
703
+ });
704
+ const recognized = await worker.recognize(preparedImage.image);
705
+ attempts.push({
706
+ confidence: Number.isFinite(recognized.data.confidence) ? recognized.data.confidence : 0,
707
+ normalizedText: normalizeWhitespace(recognized.data.text),
708
+ pageSegmentationMode,
709
+ text: recognized.data.text
710
+ });
711
+ }
712
+ return attempts;
713
+ };
714
+ const createOcrWorkerController = (defaults) => {
715
+ if (defaults?.workerMode !== "shared") {
716
+ return {
717
+ recognize: async (preparedImage, options) => {
718
+ const worker = await createOcrWorker(defaults);
719
+ try {
720
+ return await recognizeWithWorker(worker, preparedImage, options);
721
+ } finally {
722
+ await worker.terminate();
723
+ }
724
+ },
725
+ release: async () => void 0
726
+ };
727
+ }
728
+ let workerPromise;
729
+ let queue = Promise.resolve();
730
+ const getWorker = () => {
731
+ workerPromise ??= createOcrWorker(defaults);
732
+ return workerPromise;
733
+ };
734
+ const enqueue = async (operation) => {
735
+ const previous = queue;
736
+ let resolveNext = () => void 0;
737
+ queue = new Promise((resolve2) => {
738
+ resolveNext = resolve2;
739
+ });
740
+ await previous;
741
+ try {
742
+ return await operation();
743
+ } finally {
744
+ resolveNext();
745
+ }
746
+ };
747
+ return {
748
+ recognize: async (preparedImage, options) => await enqueue(
749
+ async () => recognizeWithWorker(await getWorker(), preparedImage, options)
750
+ ),
751
+ release: async () => {
752
+ await queue;
753
+ if (workerPromise === void 0) {
754
+ return;
755
+ }
756
+ const worker = await workerPromise;
757
+ workerPromise = void 0;
758
+ await worker.terminate();
759
+ }
760
+ };
761
+ };
762
+ const validateConfidence = (value, label) => {
763
+ if (!Number.isFinite(value) || value < 0 || value > 100) {
764
+ throw new TypeError(`${label} must be a number from 0 to 100.`);
765
+ }
766
+ };
767
+ const formatExpectedText = (expected) => typeof expected === "string" ? expected : expected.toString();
768
+ const serializeExpectedText = (expected) => {
769
+ if (expected === void 0) {
770
+ return void 0;
771
+ }
772
+ if (typeof expected === "string") {
773
+ return {
774
+ type: "string",
775
+ value: expected
776
+ };
777
+ }
778
+ return {
779
+ flags: expected.flags,
780
+ source: expected.source,
781
+ type: "regexp"
782
+ };
783
+ };
784
+ const selectBestOcrAttempt = (attempts) => {
785
+ const [firstAttempt] = attempts;
786
+ if (firstAttempt === void 0) {
787
+ throw new Error("OCR did not return any recognition attempts.");
788
+ }
789
+ return attempts.reduce(
790
+ (best, current) => current.confidence > best.confidence ? current : best
791
+ );
792
+ };
793
+ const createOcrTextData = (context, attempts) => {
794
+ const bestAttempt = selectBestOcrAttempt(attempts);
795
+ if (!context.artifactsEnabled) {
796
+ return {
797
+ actualImagePath: void 0,
798
+ attempts,
799
+ confidence: bestAttempt.confidence,
800
+ metadataJsonPath: void 0,
801
+ normalizedText: bestAttempt.normalizedText,
802
+ ocrInputImagePath: void 0,
803
+ outputResultPath: void 0,
804
+ pageSegmentationMode: bestAttempt.pageSegmentationMode,
805
+ text: bestAttempt.text
806
+ };
807
+ }
808
+ return {
809
+ actualImagePath: context.actualImagePath,
810
+ attempts,
811
+ confidence: bestAttempt.confidence,
812
+ metadataJsonPath: context.metadataJsonPath,
813
+ normalizedText: bestAttempt.normalizedText,
814
+ ocrInputImagePath: context.ocrInputImagePath,
815
+ outputResultPath: context.outputResultPath,
816
+ pageSegmentationMode: bestAttempt.pageSegmentationMode,
817
+ text: bestAttempt.text
818
+ };
819
+ };
820
+ const writeOcrMetadata = async (context, capture, name, matcher, options, preparedImage, attempts, expected) => {
821
+ await writeFile(
822
+ context.metadataJsonPath,
823
+ `${JSON.stringify(
824
+ {
825
+ attempts,
826
+ bounds: serializeBounds(capture.bounds),
827
+ clipped: capture.clipped,
828
+ expectedText: serializeExpectedText(expected),
829
+ imageBytes: capture.image.length,
830
+ matcher,
831
+ name,
832
+ ocrInputBytes: preparedImage.image.length,
833
+ pageSegmentationModes: options?.pageSegmentationModes,
834
+ parameters: options?.parameters,
835
+ preprocess: preparedImage.preprocess,
836
+ region: preparedImage.region,
837
+ variant: context.resolved.variant,
838
+ visibleBounds: serializeBounds(capture.visibleBounds)
839
+ },
840
+ void 0,
841
+ 2
842
+ )}
843
+ `
844
+ );
845
+ };
846
+ const saveOcrArtifactsAndMetadata = async (context, capture, name, matcher, options, preparedImage, attempts, expected) => {
847
+ if (!context.artifactsEnabled) {
848
+ return;
849
+ }
850
+ await writeFile(context.actualImagePath, capture.image);
851
+ await writeFile(context.ocrInputImagePath, preparedImage.image);
852
+ await writeOcrMetadata(
853
+ context,
854
+ capture,
855
+ name,
856
+ matcher,
857
+ options,
858
+ preparedImage,
859
+ attempts,
860
+ expected
861
+ );
862
+ };
863
+ const matchOcrAttempt = (attempt, expected, options) => {
864
+ const normalize = options?.normalizeWhitespace ?? true;
865
+ const candidate = normalize ? attempt.normalizedText : attempt.text;
866
+ const minConfidence = options?.minConfidence;
867
+ if (minConfidence !== void 0) {
868
+ validateConfidence(minConfidence, "minConfidence");
869
+ if (attempt.confidence < minConfidence) {
870
+ return false;
871
+ }
872
+ }
873
+ if (typeof expected === "string") {
874
+ if (expected.length === 0) {
875
+ throw new TypeError("expected text must not be empty.");
876
+ }
877
+ const expectedText = normalize ? normalizeWhitespace(expected) : expected;
878
+ if (expectedText.length === 0) {
879
+ throw new TypeError(
880
+ "expected text must not be empty after normalization."
881
+ );
882
+ }
883
+ if (options?.caseSensitive ?? false) {
884
+ return candidate.includes(expectedText);
885
+ }
886
+ return candidate.toLocaleLowerCase().includes(expectedText.toLocaleLowerCase());
887
+ }
888
+ expected.lastIndex = 0;
889
+ const matched = expected.test(candidate);
890
+ expected.lastIndex = 0;
891
+ return matched;
892
+ };
893
+ const createOcrResult = (data, expected, pass, selectedAttempt) => {
894
+ const artifactPaths = data.outputResultPath === void 0 ? {} : {
895
+ actualImagePath: data.actualImagePath,
896
+ metadataJsonPath: data.metadataJsonPath,
897
+ ocrInputImagePath: data.ocrInputImagePath,
898
+ outputResultPath: data.outputResultPath
899
+ };
900
+ return {
901
+ ...artifactPaths,
902
+ attempts: data.attempts,
903
+ confidence: selectedAttempt.confidence,
904
+ expectedText: formatExpectedText(expected),
905
+ normalizedText: selectedAttempt.normalizedText,
906
+ pageSegmentationMode: selectedAttempt.pageSegmentationMode,
907
+ pass,
908
+ text: selectedAttempt.text
909
+ };
910
+ };
911
+ const assertOcrText = async (data, expected, options) => {
912
+ const matchedAttempt = data.attempts.find(
913
+ (attempt) => matchOcrAttempt(attempt, expected, options)
914
+ );
915
+ if (matchedAttempt !== void 0) {
916
+ return createOcrResult(data, expected, true, matchedAttempt);
917
+ }
918
+ const bestAttempt = selectBestOcrAttempt(data.attempts);
919
+ const failedResult = createOcrResult(data, expected, false, bestAttempt);
920
+ throw createVisualError(
921
+ `Capture OCR text did not contain ${formatExpectedText(
922
+ expected
923
+ )}. Recognized text: ${JSON.stringify(bestAttempt.normalizedText)}.`,
924
+ failedResult
925
+ );
926
+ };
927
+ const createOcrText = (data) => {
928
+ const artifactPaths = data.outputResultPath === void 0 ? {} : {
929
+ actualImagePath: data.actualImagePath,
930
+ metadataJsonPath: data.metadataJsonPath,
931
+ ocrInputImagePath: data.ocrInputImagePath,
932
+ outputResultPath: data.outputResultPath
933
+ };
934
+ return {
935
+ ...artifactPaths,
936
+ attempts: data.attempts,
937
+ confidence: data.confidence,
938
+ normalizedText: data.normalizedText,
939
+ pageSegmentationMode: data.pageSegmentationMode,
940
+ text: data.text,
941
+ toContainText: async (expected, options) => await assertOcrText(data, expected, options)
942
+ };
943
+ };
944
+ const readCaptureText = async (defaults, nextCounter, workerController, capture, name, matcher, options, expected) => {
945
+ const context = await createContext(defaults, options, name, nextCounter());
946
+ const actualPng = decodePng(capture.image);
947
+ validateCaptureImage(capture, actualPng);
948
+ const ocrOptions = resolveOcrOptions(
949
+ options,
950
+ actualPng.width,
951
+ actualPng.height
952
+ );
953
+ const preparedImage = prepareOcrImage(capture.image, actualPng, ocrOptions);
954
+ const attempts = await workerController.recognize(preparedImage, ocrOptions);
955
+ await saveOcrArtifactsAndMetadata(
956
+ context,
957
+ capture,
958
+ name,
959
+ matcher,
960
+ options,
961
+ preparedImage,
962
+ attempts,
963
+ expected
964
+ );
965
+ return createOcrText(createOcrTextData(context, attempts));
966
+ };
967
+ const createLookSimilarAssertion = (defaults, nextCounter, capture, name) => {
968
+ const toLookSimilar = async (expectedImage, options) => {
969
+ const context = await createContext(defaults, options, name, nextCounter());
970
+ const actualPng = decodePng(capture.image);
971
+ validateCaptureImage(capture, actualPng);
972
+ const loadedExpectedImage = await loadExpectedImage(expectedImage);
973
+ const expectedPng = decodePng(loadedExpectedImage.data);
974
+ await saveActualAndMetadata(
975
+ context,
976
+ capture,
977
+ name,
978
+ "toLookSimilar",
979
+ options,
980
+ loadedExpectedImage
981
+ );
982
+ const comparisonOptions = resolveLookSimilarOptions(options);
983
+ const comparison = prepareComparison(actualPng, expectedPng, options);
984
+ const { diffPixels, diffPng } = createDiffPng(
985
+ comparison,
986
+ comparisonOptions.threshold
987
+ );
988
+ const diffRatio = diffPixels / comparison.totalPixels;
989
+ const result = {
990
+ ...buildBaseResult(context),
991
+ diffPixels,
992
+ diffRatio,
993
+ totalPixels: comparison.totalPixels
994
+ };
995
+ if (isLookSimilarPass(diffPixels, diffRatio, comparisonOptions)) {
996
+ return result;
997
+ }
998
+ const failureArtifacts = await writeFailureArtifacts(
999
+ context,
1000
+ loadedExpectedImage.data,
1001
+ diffPng
1002
+ );
1003
+ const failedResult = {
1004
+ ...result,
1005
+ ...failureArtifacts,
1006
+ pass: false
1007
+ };
1008
+ throw createVisualError(
1009
+ `Capture image differs: ${diffPixels} pixels (${diffRatio.toFixed(
1010
+ 6
1011
+ )}) exceeded maxDiffRatio ${comparisonOptions.maxDiffRatio}${comparisonOptions.maxDiffPixels === void 0 ? "" : ` and maxDiffPixels ${comparisonOptions.maxDiffPixels}`}.`,
1012
+ failedResult
1013
+ );
1014
+ };
1015
+ return toLookSimilar;
1016
+ };
1017
+ const createSimilarityAssertion = (defaults, nextCounter, capture, name) => {
1018
+ const toHaveSimilarity = async (expectedImage, options) => {
1019
+ const context = await createContext(defaults, options, name, nextCounter());
1020
+ const actualPng = decodePng(capture.image);
1021
+ validateCaptureImage(capture, actualPng);
1022
+ const loadedExpectedImage = await loadExpectedImage(expectedImage);
1023
+ const expectedPng = decodePng(loadedExpectedImage.data);
1024
+ await saveActualAndMetadata(
1025
+ context,
1026
+ capture,
1027
+ name,
1028
+ "toHaveSimilarity",
1029
+ options,
1030
+ loadedExpectedImage
1031
+ );
1032
+ const comparisonOptions = resolveSimilarityOptions(options);
1033
+ const comparison = prepareComparison(actualPng, expectedPng, options);
1034
+ const similarity = ssim(
1035
+ {
1036
+ data: new Uint8ClampedArray(comparison.expectedData),
1037
+ height: comparison.height,
1038
+ width: comparison.width
1039
+ },
1040
+ {
1041
+ data: new Uint8ClampedArray(comparison.actualData),
1042
+ height: comparison.height,
1043
+ width: comparison.width
1044
+ }
1045
+ ).mssim;
1046
+ const { diffPixels, diffPng } = createDiffPng(comparison, 0.1);
1047
+ const diffRatio = diffPixels / comparison.totalPixels;
1048
+ const result = {
1049
+ ...buildBaseResult(context),
1050
+ diffPixels,
1051
+ diffRatio,
1052
+ similarity,
1053
+ totalPixels: comparison.totalPixels
1054
+ };
1055
+ if (similarity >= comparisonOptions.minSimilarity) {
1056
+ return result;
1057
+ }
1058
+ const failureArtifacts = await writeFailureArtifacts(
1059
+ context,
1060
+ loadedExpectedImage.data,
1061
+ diffPng
1062
+ );
1063
+ const failedResult = {
1064
+ ...result,
1065
+ ...failureArtifacts,
1066
+ pass: false
1067
+ };
1068
+ throw createVisualError(
1069
+ `Capture image similarity ${similarity.toFixed(
1070
+ 6
1071
+ )} is below minSimilarity ${comparisonOptions.minSimilarity}.`,
1072
+ failedResult
1073
+ );
1074
+ };
1075
+ return toHaveSimilarity;
1076
+ };
1077
+ const createReadTextAssertion = (defaults, nextCounter, workerController, capture, name) => {
1078
+ const readText = async (options) => await readCaptureText(
1079
+ defaults,
1080
+ nextCounter,
1081
+ workerController,
1082
+ capture,
1083
+ name,
1084
+ "readText",
1085
+ options,
1086
+ void 0
1087
+ );
1088
+ return readText;
1089
+ };
1090
+ const createContainTextAssertion = (defaults, nextCounter, workerController, capture, name) => {
1091
+ const toContainText = async (expected, options) => {
1092
+ const ocrText = await readCaptureText(
1093
+ defaults,
1094
+ nextCounter,
1095
+ workerController,
1096
+ capture,
1097
+ name,
1098
+ "toContainText",
1099
+ options,
1100
+ expected
1101
+ );
1102
+ return await ocrText.toContainText(expected, options);
1103
+ };
1104
+ return toContainText;
1105
+ };
1106
+ const createGtkCaptureExpect = (defaults) => {
1107
+ const resolvedDefaults = defaults ?? {};
1108
+ const workerController = createOcrWorkerController(resolvedDefaults.ocr);
1109
+ let counter = 0;
1110
+ const nextCounter = () => {
1111
+ const value = counter;
1112
+ counter += 1;
1113
+ return value;
1114
+ };
1115
+ const release = async () => {
1116
+ await workerController.release();
1117
+ };
1118
+ const expectCapture2 = (capture, name) => ({
1119
+ readText: createReadTextAssertion(
1120
+ resolvedDefaults,
1121
+ nextCounter,
1122
+ workerController,
1123
+ capture,
1124
+ name
1125
+ ),
1126
+ toContainText: createContainTextAssertion(
1127
+ resolvedDefaults,
1128
+ nextCounter,
1129
+ workerController,
1130
+ capture,
1131
+ name
1132
+ ),
1133
+ toHaveSimilarity: createSimilarityAssertion(
1134
+ resolvedDefaults,
1135
+ nextCounter,
1136
+ capture,
1137
+ name
1138
+ ),
1139
+ toLookSimilar: createLookSimilarAssertion(
1140
+ resolvedDefaults,
1141
+ nextCounter,
1142
+ capture,
1143
+ name
1144
+ )
1145
+ });
1146
+ return {
1147
+ expectCapture: expectCapture2,
1148
+ release,
1149
+ [Symbol.asyncDispose]: release
1150
+ };
1151
+ };
1152
+ const defaultGtkCaptureExpect = createGtkCaptureExpect();
1153
+ const expectCapture = (capture, name) => defaultGtkCaptureExpect.expectCapture(capture, name);
1154
+ export {
1155
+ createGtkCaptureExpect,
1156
+ expectCapture
1157
+ };
1158
+ //# sourceMappingURL=testing.mjs.map