// 🤖 Tests for the web BlurHash codec (web/static/blurhash.js) — including THE // cross-implementation vector: the Swift twin (Blurhash.swift) encodes the // same deterministic pixel grid in BlurhashTests.swift and must produce the // SAME hash. If either side changes output, both tests fail together. // node test/blurhash.test.mjs import assert from "node:assert/strict"; import { encodeBlurhash, decodeBlurhash } from "../web/static/blurhash.js"; let failures = 0; async function test(name, fn) { try { await fn(); console.log("PASS " + name); } catch (e) { console.log("FAIL " + name + " — " + (e && e.message)); failures++; } } // The shared vector grid: 8×6, RGBA, deterministic bytes — red ramps left to // right, green top to bottom, blue fixed. Keep IDENTICAL to vectorImage() in // BlurhashTests.swift. function vectorGrid() { const w = 8, h = 6; const px = new Uint8ClampedArray(w * h * 4); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { const o = (y * w + x) * 4; px[o] = Math.round((x * 255) / (w - 1)); px[o + 1] = Math.round((y * 255) / (h - 1)); px[o + 2] = 64; px[o + 3] = 255; } } return { px, w, h }; } await test("cross-implementation vector (pinned against Blurhash.swift)", () => { const { px, w, h } = vectorGrid(); // The one true hash for the vector grid — BlurhashTests.swift pins the same // string. A change here without the twin is a cross-platform drift. assert.equal(encodeBlurhash(px, w, h), "LyI5d=39a|%0z2NKfQnSeXf7fQf7"); }); await test("solid colour round-trips through encode/decode", () => { const w = 16, h = 12; const px = new Uint8ClampedArray(w * h * 4); for (let i = 0; i < px.length; i += 4) { px[i] = 200; px[i + 1] = 60; px[i + 2] = 40; px[i + 3] = 255; } const hash = encodeBlurhash(px, w, h); const out = decodeBlurhash(hash, 16, 12); let r = 0, g = 0, b = 0; for (let i = 0; i < out.length; i += 4) { r += out[i]; g += out[i + 1]; b += out[i + 2]; } const n = w * h; assert.ok(Math.abs(r / n - 200) < 12, `red ${r / n}`); assert.ok(Math.abs(g / n - 60) < 12, `green ${g / n}`); assert.ok(Math.abs(b / n - 40) < 12, `blue ${b / n}`); }); await test("gradient keeps its direction", () => { const w = 16, h = 12; const px = new Uint8ClampedArray(w * h * 4); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { const o = (y * w + x) * 4; px[o] = x < w / 2 ? 255 : 0; px[o + 2] = x < w / 2 ? 0 : 255; px[o + 3] = 255; } } const out = decodeBlurhash(encodeBlurhash(px, w, h), 16, 12); const at = (x, y) => out.slice((y * 16 + x) * 4, (y * 16 + x) * 4 + 4); const left = at(1, 6), right = at(14, 6); assert.ok(left[0] > left[2], "left reads red"); assert.ok(right[2] > right[0], "right reads blue"); }); await test("malformed hashes are rejected", () => { assert.equal(decodeBlurhash(""), null); assert.equal(decodeBlurhash("LEHV6"), null); assert.equal(decodeBlurhash("LEHV6nWB2yk8pyo0adR*.7kCMdnj!!"), null); }); console.log(failures ? `\n${failures} FAILED` : "\nALL PASSED"); process.exit(failures ? 1 : 0);