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