mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Fix 2 failing unit tests (landing hero text mismatch) and broken coverage tooling (brace-expansion v5 override breaking minimatch). Add ~1,097 new test cases via 14-agent parallel expansion: - Unit: +290 tests (AI bridge, image-engine, stores, API helpers) - Integration: +504 tests (all tools, cross-format matrix, adversarial) - E2E: +363 tests (navigation, tool UI, batch/pipeline, settings, visual regression, accessibility, performance, cross-browser) Total: 4,223 unit + 6,057 integration + 1,563 E2E = 11,843 tests
255 lines
7.8 KiB
TypeScript
255 lines
7.8 KiB
TypeScript
/**
|
|
* Expanded QOI codec tests covering:
|
|
* - LUMA encoding path (medium deltas)
|
|
* - INDEX encoding path (repeated colors)
|
|
* - RGBA encoding path (changing alpha)
|
|
* - RGB encoding path (large deltas, same alpha)
|
|
* - DIFF encoding path (small deltas)
|
|
* - RUN encoding path (max run length 62)
|
|
* - Run ending at last pixel vs mid-stream
|
|
* - 3-channel encoding with alpha fixed at 255
|
|
* - Edge cases: large images, alternating patterns
|
|
*/
|
|
import { qoiDecode, qoiEncode } from "@snapotter/image-engine";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
describe("QOI expanded codec paths", () => {
|
|
it("exercises DIFF encoding for small color changes", () => {
|
|
// Create pixels where each successive pixel differs by small amounts (-2..+1)
|
|
const w = 4;
|
|
const h = 1;
|
|
const pixels = new Uint8Array(w * 4);
|
|
// Pixel 0: (100, 100, 100, 255)
|
|
pixels[0] = 100;
|
|
pixels[1] = 100;
|
|
pixels[2] = 100;
|
|
pixels[3] = 255;
|
|
// Pixel 1: (101, 99, 100, 255) -- dr=1, dg=-1, db=0 -- fits DIFF
|
|
pixels[4] = 101;
|
|
pixels[5] = 99;
|
|
pixels[6] = 100;
|
|
pixels[7] = 255;
|
|
// Pixel 2: (100, 100, 101, 255) -- dr=-1, dg=1, db=1 -- fits DIFF
|
|
pixels[8] = 100;
|
|
pixels[9] = 100;
|
|
pixels[10] = 101;
|
|
pixels[11] = 255;
|
|
// Pixel 3: (98, 101, 100, 255) -- dr=-2, dg=1, db=-1 -- fits DIFF
|
|
pixels[12] = 98;
|
|
pixels[13] = 101;
|
|
pixels[14] = 100;
|
|
pixels[15] = 255;
|
|
|
|
const encoded = qoiEncode(pixels, w, h, 4);
|
|
const { pixels: decoded } = qoiDecode(encoded);
|
|
for (let i = 0; i < pixels.length; i++) {
|
|
expect(decoded[i]).toBe(pixels[i]);
|
|
}
|
|
});
|
|
|
|
it("exercises LUMA encoding for medium color changes", () => {
|
|
// Deltas that fit LUMA but not DIFF (dg in -32..31, drDg and dbDg in -8..7)
|
|
const w = 3;
|
|
const h = 1;
|
|
const pixels = new Uint8Array(w * 4);
|
|
pixels[0] = 100;
|
|
pixels[1] = 100;
|
|
pixels[2] = 100;
|
|
pixels[3] = 255;
|
|
// dg=10, dr=10+5=15, db=10-3=7 -- LUMA range
|
|
pixels[4] = 115;
|
|
pixels[5] = 110;
|
|
pixels[6] = 107;
|
|
pixels[7] = 255;
|
|
// dg=-20, dr=-20+2=-18, db=-20-5=-25 -- LUMA range
|
|
pixels[8] = 97;
|
|
pixels[9] = 90;
|
|
pixels[10] = 82;
|
|
pixels[11] = 255;
|
|
|
|
const encoded = qoiEncode(pixels, w, h, 4);
|
|
const { pixels: decoded } = qoiDecode(encoded);
|
|
for (let i = 0; i < pixels.length; i++) {
|
|
expect(decoded[i]).toBe(pixels[i]);
|
|
}
|
|
});
|
|
|
|
it("exercises RGB encoding for large color changes with same alpha", () => {
|
|
// Deltas too large for DIFF or LUMA, but alpha stays the same
|
|
const w = 2;
|
|
const h = 1;
|
|
const pixels = new Uint8Array(w * 4);
|
|
pixels[0] = 10;
|
|
pixels[1] = 20;
|
|
pixels[2] = 30;
|
|
pixels[3] = 255;
|
|
// Large jump: dr=200, dg=200, db=200 -- falls through to RGB
|
|
pixels[4] = 210;
|
|
pixels[5] = 220;
|
|
pixels[6] = 230;
|
|
pixels[7] = 255;
|
|
|
|
const encoded = qoiEncode(pixels, w, h, 4);
|
|
const { pixels: decoded } = qoiDecode(encoded);
|
|
for (let i = 0; i < pixels.length; i++) {
|
|
expect(decoded[i]).toBe(pixels[i]);
|
|
}
|
|
// Verify size: header (14) + first pixel RGB op (4) + second pixel RGB op (4) + end marker (8)
|
|
// Actually first pixel deviates from prev (0,0,0,255), so it gets some op
|
|
expect(encoded.length).toBeGreaterThan(14 + 8);
|
|
});
|
|
|
|
it("exercises RGBA encoding for alpha changes", () => {
|
|
const w = 3;
|
|
const h = 1;
|
|
const pixels = new Uint8Array(w * 4);
|
|
pixels[0] = 100;
|
|
pixels[1] = 100;
|
|
pixels[2] = 100;
|
|
pixels[3] = 255;
|
|
pixels[4] = 100;
|
|
pixels[5] = 100;
|
|
pixels[6] = 100;
|
|
pixels[7] = 128; // alpha changed
|
|
pixels[8] = 100;
|
|
pixels[9] = 100;
|
|
pixels[10] = 100;
|
|
pixels[11] = 0; // alpha changed again
|
|
|
|
const encoded = qoiEncode(pixels, w, h, 4);
|
|
const { pixels: decoded } = qoiDecode(encoded);
|
|
for (let i = 0; i < pixels.length; i++) {
|
|
expect(decoded[i]).toBe(pixels[i]);
|
|
}
|
|
});
|
|
|
|
it("exercises INDEX encoding for repeated colors", () => {
|
|
// Use the same two colors alternating -- after first occurrence they should be in index
|
|
const w = 8;
|
|
const h = 1;
|
|
const pixels = new Uint8Array(w * 4);
|
|
for (let i = 0; i < w; i++) {
|
|
const isEven = i % 2 === 0;
|
|
pixels[i * 4] = isEven ? 200 : 50;
|
|
pixels[i * 4 + 1] = isEven ? 100 : 150;
|
|
pixels[i * 4 + 2] = isEven ? 50 : 200;
|
|
pixels[i * 4 + 3] = 255;
|
|
}
|
|
|
|
const encoded = qoiEncode(pixels, w, h, 4);
|
|
const { pixels: decoded } = qoiDecode(encoded);
|
|
for (let i = 0; i < pixels.length; i++) {
|
|
expect(decoded[i]).toBe(pixels[i]);
|
|
}
|
|
// With INDEX ops, the encoded size should be much smaller than raw
|
|
expect(encoded.length).toBeLessThan(w * 5 + 14 + 8);
|
|
});
|
|
|
|
it("exercises maximum RUN length of 62", () => {
|
|
// 63 identical pixels: first one is encoded, then a run of 62, then 1 more
|
|
const w = 63;
|
|
const h = 1;
|
|
const pixels = new Uint8Array(w * 4);
|
|
for (let i = 0; i < w; i++) {
|
|
pixels[i * 4] = 77;
|
|
pixels[i * 4 + 1] = 88;
|
|
pixels[i * 4 + 2] = 99;
|
|
pixels[i * 4 + 3] = 255;
|
|
}
|
|
|
|
const encoded = qoiEncode(pixels, w, h, 4);
|
|
const { pixels: decoded } = qoiDecode(encoded);
|
|
for (let i = 0; i < pixels.length; i++) {
|
|
expect(decoded[i]).toBe(pixels[i]);
|
|
}
|
|
});
|
|
|
|
it("run ending at last pixel produces correct output", () => {
|
|
// Exactly 62 identical pixels -- run fills exactly one RUN op
|
|
const w = 62;
|
|
const h = 1;
|
|
const pixels = new Uint8Array(w * 4);
|
|
for (let i = 0; i < w; i++) {
|
|
pixels[i * 4] = 33;
|
|
pixels[i * 4 + 1] = 44;
|
|
pixels[i * 4 + 2] = 55;
|
|
pixels[i * 4 + 3] = 255;
|
|
}
|
|
|
|
const { pixels: decoded } = qoiDecode(qoiEncode(pixels, w, h, 4));
|
|
for (let i = 0; i < pixels.length; i++) {
|
|
expect(decoded[i]).toBe(pixels[i]);
|
|
}
|
|
});
|
|
|
|
it("3-channel encoding uses 255 for alpha in output", () => {
|
|
const w = 4;
|
|
const h = 1;
|
|
const pixels = new Uint8Array(w * 3);
|
|
for (let i = 0; i < pixels.length; i++) {
|
|
pixels[i] = (i * 37) & 0xff;
|
|
}
|
|
|
|
const encoded = qoiEncode(pixels, w, h, 3);
|
|
const { header, pixels: decoded } = qoiDecode(encoded);
|
|
expect(header.channels).toBe(3);
|
|
// Every decoded pixel should have alpha=255
|
|
for (let i = 0; i < w; i++) {
|
|
expect(decoded[i * 4 + 3]).toBe(255);
|
|
}
|
|
});
|
|
|
|
it("round-trips large image (100x100) with varied data", () => {
|
|
const w = 100;
|
|
const h = 100;
|
|
const pixels = new Uint8Array(w * h * 4);
|
|
for (let i = 0; i < w * h; i++) {
|
|
pixels[i * 4] = (i * 3) & 0xff;
|
|
pixels[i * 4 + 1] = (i * 7 + 11) & 0xff;
|
|
pixels[i * 4 + 2] = (i * 13 + 23) & 0xff;
|
|
pixels[i * 4 + 3] = 255;
|
|
}
|
|
|
|
const { pixels: decoded } = qoiDecode(qoiEncode(pixels, w, h, 4));
|
|
for (let i = 0; i < pixels.length; i++) {
|
|
expect(decoded[i]).toBe(pixels[i]);
|
|
}
|
|
});
|
|
|
|
it("handles checkerboard pattern (many INDEX lookups)", () => {
|
|
const w = 10;
|
|
const h = 10;
|
|
const pixels = new Uint8Array(w * h * 4);
|
|
for (let y = 0; y < h; y++) {
|
|
for (let x = 0; x < w; x++) {
|
|
const idx = (y * w + x) * 4;
|
|
const isBlack = (x + y) % 2 === 0;
|
|
pixels[idx] = isBlack ? 0 : 255;
|
|
pixels[idx + 1] = isBlack ? 0 : 255;
|
|
pixels[idx + 2] = isBlack ? 0 : 255;
|
|
pixels[idx + 3] = 255;
|
|
}
|
|
}
|
|
|
|
const { pixels: decoded } = qoiDecode(qoiEncode(pixels, w, h, 4));
|
|
for (let i = 0; i < pixels.length; i++) {
|
|
expect(decoded[i]).toBe(pixels[i]);
|
|
}
|
|
});
|
|
|
|
it("handles all-zero pixels (black with full alpha)", () => {
|
|
const w = 5;
|
|
const h = 5;
|
|
const pixels = new Uint8Array(w * h * 4);
|
|
// All zeros except alpha set to 255
|
|
for (let i = 0; i < w * h; i++) {
|
|
pixels[i * 4 + 3] = 255;
|
|
}
|
|
|
|
const { pixels: decoded } = qoiDecode(qoiEncode(pixels, w, h, 4));
|
|
for (let i = 0; i < pixels.length; i++) {
|
|
expect(decoded[i]).toBe(pixels[i]);
|
|
}
|
|
});
|
|
});
|