| 1 | // 🤖 Tests for the web BlurHash codec (web/static/blurhash.js) — including THE |
| 2 | // cross-implementation vector: the Swift twin (Blurhash.swift) encodes the |
| 3 | // same deterministic pixel grid in BlurhashTests.swift and must produce the |
| 4 | // SAME hash. If either side changes output, both tests fail together. |
| 5 | // node test/blurhash.test.mjs |
| 6 | import assert from "node:assert/strict"; |
| 7 | import { encodeBlurhash, decodeBlurhash } from "../web/static/blurhash.js"; |
| 8 | |
| 9 | let failures = 0; |
| 10 | async function test(name, fn) { |
| 11 | try { |
| 12 | await fn(); |
| 13 | console.log("PASS " + name); |
| 14 | } catch (e) { |
| 15 | console.log("FAIL " + name + " — " + (e && e.message)); |
| 16 | failures++; |
| 17 | } |
| 18 | } |
| 19 | |
| 20 | // The shared vector grid: 8×6, RGBA, deterministic bytes — red ramps left to |
| 21 | // right, green top to bottom, blue fixed. Keep IDENTICAL to vectorImage() in |
| 22 | // BlurhashTests.swift. |
| 23 | function vectorGrid() { |
| 24 | const w = 8, h = 6; |
| 25 | const px = new Uint8ClampedArray(w * h * 4); |
| 26 | for (let y = 0; y < h; y++) { |
| 27 | for (let x = 0; x < w; x++) { |
| 28 | const o = (y * w + x) * 4; |
| 29 | px[o] = Math.round((x * 255) / (w - 1)); |
| 30 | px[o + 1] = Math.round((y * 255) / (h - 1)); |
| 31 | px[o + 2] = 64; |
| 32 | px[o + 3] = 255; |
| 33 | } |
| 34 | } |
| 35 | return { px, w, h }; |
| 36 | } |
| 37 | |
| 38 | await test("cross-implementation vector (pinned against Blurhash.swift)", () => { |
| 39 | const { px, w, h } = vectorGrid(); |
| 40 | // The one true hash for the vector grid — BlurhashTests.swift pins the same |
| 41 | // string. A change here without the twin is a cross-platform drift. |
| 42 | assert.equal(encodeBlurhash(px, w, h), "LyI5d=39a|%0z2NKfQnSeXf7fQf7"); |
| 43 | }); |
| 44 | |
| 45 | await test("solid colour round-trips through encode/decode", () => { |
| 46 | const w = 16, h = 12; |
| 47 | const px = new Uint8ClampedArray(w * h * 4); |
| 48 | for (let i = 0; i < px.length; i += 4) { |
| 49 | px[i] = 200; px[i + 1] = 60; px[i + 2] = 40; px[i + 3] = 255; |
| 50 | } |
| 51 | const hash = encodeBlurhash(px, w, h); |
| 52 | const out = decodeBlurhash(hash, 16, 12); |
| 53 | let r = 0, g = 0, b = 0; |
| 54 | for (let i = 0; i < out.length; i += 4) { r += out[i]; g += out[i + 1]; b += out[i + 2]; } |
| 55 | const n = w * h; |
| 56 | assert.ok(Math.abs(r / n - 200) < 12, `red ${r / n}`); |
| 57 | assert.ok(Math.abs(g / n - 60) < 12, `green ${g / n}`); |
| 58 | assert.ok(Math.abs(b / n - 40) < 12, `blue ${b / n}`); |
| 59 | }); |
| 60 | |
| 61 | await test("gradient keeps its direction", () => { |
| 62 | const w = 16, h = 12; |
| 63 | const px = new Uint8ClampedArray(w * h * 4); |
| 64 | for (let y = 0; y < h; y++) { |
| 65 | for (let x = 0; x < w; x++) { |
| 66 | const o = (y * w + x) * 4; |
| 67 | px[o] = x < w / 2 ? 255 : 0; |
| 68 | px[o + 2] = x < w / 2 ? 0 : 255; |
| 69 | px[o + 3] = 255; |
| 70 | } |
| 71 | } |
| 72 | const out = decodeBlurhash(encodeBlurhash(px, w, h), 16, 12); |
| 73 | const at = (x, y) => out.slice((y * 16 + x) * 4, (y * 16 + x) * 4 + 4); |
| 74 | const left = at(1, 6), right = at(14, 6); |
| 75 | assert.ok(left[0] > left[2], "left reads red"); |
| 76 | assert.ok(right[2] > right[0], "right reads blue"); |
| 77 | }); |
| 78 | |
| 79 | await test("malformed hashes are rejected", () => { |
| 80 | assert.equal(decodeBlurhash(""), null); |
| 81 | assert.equal(decodeBlurhash("LEHV6"), null); |
| 82 | assert.equal(decodeBlurhash("LEHV6nWB2yk8pyo0adR*.7kCMdnj!!"), null); |
| 83 | }); |
| 84 | |
| 85 | console.log(failures ? `\n${failures} FAILED` : "\nALL PASSED"); |
| 86 | process.exit(failures ? 1 : 0); |
| 87 | |