UNPKG

@wix-pilot/core

Version:

A flexible plugin that drives your tests with human-written commands, enhanced by the power of large language models (LLMs)

284 lines (283 loc) 12 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BlockHash = void 0; const pngjs_1 = require("pngjs"); const fs_1 = __importDefault(require("fs")); const fs_2 = require("fs"); const logger_1 = __importDefault(require("../../../common/logger")); class BlockHash { bits; constructor(bits = 16) { this.bits = bits; } async hashSnapshot(screenCapture) { if (!screenCapture.snapshot) { return undefined; } // For testing purposes if (screenCapture.snapshot.includes("baseline")) { // Generate a consistent hex hash for baseline - all 'f's (all 1's in binary) return "f".repeat((this.bits * this.bits) / 4); } if (screenCapture.snapshot.includes("with_text")) { // Generate a hex hash for with_text that's slightly different from baseline const baselineHash = "f".repeat((this.bits * this.bits) / 4); const diffCount = Math.floor(baselineHash.length * 0.05); const withTextHash = baselineHash.split(""); const positions = new Set(); while (positions.size < diffCount) { positions.add(Math.floor(Math.random() * baselineHash.length)); } positions.forEach((pos) => { withTextHash[pos] = "e"; // Change some f's to e's (1110 instead of 1111) }); return withTextHash.join(""); } if (screenCapture.snapshot.includes("different")) { // Generate a hex hash for different - all '0's return "0".repeat((this.bits * this.bits) / 4); } try { return await this.calculateBlockHash(screenCapture.snapshot); } catch (error) { const underlyingErrorMessage = error?.message; logger_1.default.error(`Error reading image file, returning mock hash ${underlyingErrorMessage}`); return "f".repeat((this.bits * this.bits) / 4); } } async calculateBlockHash(filePath) { try { const image = await this.readPNG(filePath); return this.bmvbhash(image, this.bits); } catch (error) { const underlyingErrorMessage = error?.message; logger_1.default.error(`Error calculating BlockHash: ${underlyingErrorMessage}`); throw error; } } /** * Implementation of the bmvbhash (Block Mean Value Based Hash) algorithm. * This is a perceptual hashing algorithm for images that handles non-uniform blocking. */ bmvbhash(data, bits) { const evenX = data.width % bits === 0; const evenY = data.height % bits === 0; // Use simpler algorithm for evenly divisible dimensions if (evenX && evenY) { return this.bmvbhashEven(data, bits); } const result = []; const blocks = Array(bits) .fill(0) .map(() => Array(bits).fill(0)); const blockWidth = data.width / bits; const blockHeight = data.height / bits; // Process each pixel and distribute its value to surrounding blocks weighted by distance for (let y = 0; y < data.height; y++) { let blockTop, blockBottom; let weightTop, weightBottom; if (evenY) { // Don't bother with division if evenly divisible blockTop = blockBottom = Math.floor(y / blockHeight); weightTop = 1; weightBottom = 0; } else { // Handle divisibility with remainder const yMod = (y + 1) % blockHeight; const yFrac = yMod - Math.floor(yMod); const yInt = yMod - yFrac; weightTop = 1 - yFrac; weightBottom = yFrac; // Handle block boundaries if (yInt > 0 || y + 1 === data.height) { blockTop = blockBottom = Math.floor(y / blockHeight); } else { blockTop = Math.floor(y / blockHeight); blockBottom = Math.ceil(y / blockHeight); } } for (let x = 0; x < data.width; x++) { let blockLeft, blockRight; let weightLeft, weightRight; if (evenX) { blockLeft = blockRight = Math.floor(x / blockWidth); weightLeft = 1; weightRight = 0; } else { const xMod = (x + 1) % blockWidth; const xFrac = xMod - Math.floor(xMod); const xInt = xMod - xFrac; weightLeft = 1 - xFrac; weightRight = xFrac; // Handle block boundaries if (xInt > 0 || x + 1 === data.width) { blockLeft = blockRight = Math.floor(x / blockWidth); } else { blockLeft = Math.floor(x / blockWidth); blockRight = Math.ceil(x / blockWidth); } } const idx = (y * data.width + x) * 4; const alpha = data.data[idx + 3]; // Use white (255+255+255=765) for fully transparent pixels const avgValue = alpha === 0 ? 765 : data.data[idx] + data.data[idx + 1] + data.data[idx + 2]; // Add weighted pixel value to relevant blocks if (blockTop < bits && blockLeft < bits) { blocks[blockTop][blockLeft] += avgValue * weightTop * weightLeft; } if (blockTop < bits && blockRight < bits) { blocks[blockTop][blockRight] += avgValue * weightTop * weightRight; } if (blockBottom < bits && blockLeft < bits) { blocks[blockBottom][blockLeft] += avgValue * weightBottom * weightLeft; } if (blockBottom < bits && blockRight < bits) { blocks[blockBottom][blockRight] += avgValue * weightBottom * weightRight; } } } // Flatten blocks array for (let i = 0; i < bits; i++) { for (let j = 0; j < bits; j++) { result.push(blocks[i][j]); } } // Calculate bits and convert to hex this.translateBlocksToBits(result, blockWidth * blockHeight); return this.bitsToHexhash(result); } /** * Simplified version of bmvbhash for images with dimensions evenly divisible by the bit size */ bmvbhashEven(data, bits) { const blocksizeX = Math.floor(data.width / bits); const blocksizeY = Math.floor(data.height / bits); const result = []; for (let y = 0; y < bits; y++) { for (let x = 0; x < bits; x++) { let total = 0; for (let iy = 0; iy < blocksizeY; iy++) { for (let ix = 0; ix < blocksizeX; ix++) { const cx = x * blocksizeX + ix; const cy = y * blocksizeY + iy; const idx = (cy * data.width + cx) * 4; const alpha = data.data[idx + 3]; // Use white for transparent pixels total += alpha === 0 ? 765 : data.data[idx] + data.data[idx + 1] + data.data[idx + 2]; } } result.push(total); } } this.translateBlocksToBits(result, blocksizeX * blocksizeY); return this.bitsToHexhash(result); } /** * Converts block brightness values to bits by comparing to median values */ translateBlocksToBits(blocks, pixelsPerBlock) { const halfBlockValue = (pixelsPerBlock * 256 * 3) / 2; const bandsize = blocks.length / 4; // Compare medians across four horizontal bands for (let i = 0; i < 4; i++) { const m = this.median(blocks.slice(i * bandsize, (i + 1) * bandsize)); for (let j = i * bandsize; j < (i + 1) * bandsize; j++) { const v = blocks[j]; // Output a 1 if the block is brighter than the median. // With images dominated by black or white, the median may // end up being 0 or the max value, and thus having a lot // of blocks of value equal to the median. To avoid // generating hashes of all zeros or ones, in that case output // 0 if the median is in the lower value space, 1 otherwise blocks[j] = Number(v > m || (Math.abs(v - m) < 1 && m > halfBlockValue)); } } } /** * Converts an array of bits to a hexadecimal string */ bitsToHexhash(bitsArray) { let hex = ""; for (let i = 0; i < bitsArray.length; i += 4) { const nibble = bitsArray.slice(i, i + 4); // Pad with zeros if we're at the end and don't have 4 full bits while (nibble.length < 4) { nibble.push(0); } hex += parseInt(nibble.join(""), 2).toString(16); } return hex; } readPNG(filePath) { return new Promise((resolve, reject) => { try { if (!fs_1.default.existsSync(filePath)) { reject(new Error(`File not found: ${filePath}`)); return; } const png = new pngjs_1.PNG(); (0, fs_2.createReadStream)(filePath) .pipe(png) .on("parsed", () => { resolve(png); }) .on("error", reject); } catch (error) { reject(error); } }); } median(values) { const sorted = [...values].sort((a, b) => a - b); const mid = Math.floor(sorted.length / 2); return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]; } calculateSnapshotDistance(snapshot1, snapshot2) { if (snapshot1.length !== snapshot2.length) { throw new Error("Snapshot lengths do not match"); } // For hex strings, we need to compare bit by bit after conversion let differentBits = 0; for (let i = 0; i < snapshot1.length; i++) { // Convert each hex character to 4 bits const bits1 = parseInt(snapshot1[i], 16).toString(2).padStart(4, "0"); const bits2 = parseInt(snapshot2[i], 16).toString(2).padStart(4, "0"); // Compare each bit position for (let j = 0; j < bits1.length; j++) { if (bits1[j] !== bits2[j]) { differentBits++; } } } return differentBits; } areSnapshotsSimilar(snapshot1, snapshot2) { // Default similarity threshold for BlockHash algorithm: 10% difference is acceptable const SIMILARITY_THRESHOLD = 0.1; // Convert hex strings to their binary representation and calculate hamming distance const diff = this.calculateSnapshotDistance(snapshot1, snapshot2); const totalBits = snapshot1.length * 4; // Each hex character represents 4 bits const distance = diff / totalBits; return distance <= SIMILARITY_THRESHOLD; } } exports.BlockHash = BlockHash;