@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
JavaScript
;
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;