You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
268 lines
9.0 KiB
268 lines
9.0 KiB
// =============================================================================
|
|
// library.test.ts — bun test suite for MonoDisplay library
|
|
//
|
|
// run: bun test src/library.test.ts
|
|
//
|
|
// Tests are grouped by component. Canvas-dependent renderer tests use a
|
|
// minimal mock so they run in Bun (no DOM). Parser + BinaryReader tests are
|
|
// pure TypeScript with no mocks needed.
|
|
// =============================================================================
|
|
|
|
import { test, expect, describe, mock, beforeEach } from "bun:test";
|
|
import {
|
|
BinaryReader,
|
|
MonoDisplayParser,
|
|
buildBinBuffer,
|
|
type StaticFrame,
|
|
type Animation,
|
|
type TextDisplay,
|
|
type ScrollText,
|
|
} from "../src/library";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers to build raw binary buffers for each content type
|
|
// (mirrors the format defined in library.ts header comment)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const MAGIC_BYTES = new Uint8Array([0x4d, 0x4f, 0x4e, 0x4f]); // "MONO"
|
|
|
|
function makeHeader(contentType: number, width: number, height: number): Uint8Array {
|
|
const h = new Uint8Array(10);
|
|
h.set(MAGIC_BYTES, 0);
|
|
h[4] = 1; // version
|
|
h[5] = contentType;
|
|
new DataView(h.buffer).setUint16(6, width, true);
|
|
new DataView(h.buffer).setUint16(8, height, true);
|
|
return h;
|
|
}
|
|
|
|
function concat(...parts: Uint8Array[]): ArrayBuffer {
|
|
const total = parts.reduce((s, p) => s + p.byteLength, 0);
|
|
const out = new Uint8Array(total);
|
|
let off = 0;
|
|
for (const p of parts) { out.set(p, off); off += p.byteLength; }
|
|
return out.buffer;
|
|
}
|
|
|
|
function u16le(n: number): Uint8Array {
|
|
const b = new Uint8Array(2);
|
|
new DataView(b.buffer).setUint16(0, n, true);
|
|
return b;
|
|
}
|
|
|
|
function u32le(n: number): Uint8Array {
|
|
const b = new Uint8Array(4);
|
|
new DataView(b.buffer).setUint32(0, n, true);
|
|
return b;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// BinaryReader
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("BinaryReader", () => {
|
|
test("readByte reads single byte", () => {
|
|
const r = new BinaryReader(new Uint8Array([0xAB]).buffer);
|
|
expect(r.readByte()).toBe(0xAB);
|
|
expect(r.remaining).toBe(0);
|
|
});
|
|
|
|
test("readUint16 little-endian", () => {
|
|
const r = new BinaryReader(new Uint8Array([0x34, 0x12]).buffer);
|
|
expect(r.readUint16()).toBe(0x1234);
|
|
});
|
|
|
|
test("readShort is alias for readUint16", () => {
|
|
const r = new BinaryReader(new Uint8Array([0x01, 0x00]).buffer);
|
|
expect(r.readShort()).toBe(1);
|
|
});
|
|
|
|
test("readUint32 little-endian", () => {
|
|
const r = new BinaryReader(new Uint8Array([0x78, 0x56, 0x34, 0x12]).buffer);
|
|
expect(r.readUint32()).toBe(0x12345678);
|
|
});
|
|
|
|
test("readUint64 little-endian", () => {
|
|
const bytes = new Uint8Array(8);
|
|
new DataView(bytes.buffer).setBigUint64(0, 0x0102030405060708n, true);
|
|
const r = new BinaryReader(bytes.buffer);
|
|
expect(r.readUint64()).toBe(0x0102030405060708n);
|
|
});
|
|
|
|
test("offset advances correctly", () => {
|
|
const r = new BinaryReader(new Uint8Array([1, 2, 3, 4, 5, 6]).buffer);
|
|
r.readByte();
|
|
expect(r.offset).toBe(1);
|
|
r.readUint16();
|
|
expect(r.offset).toBe(3);
|
|
expect(() => r.readUint32()).toThrow(RangeError); // 3 bytes remain, need 4
|
|
});
|
|
|
|
test("RangeError when buffer exhausted", () => {
|
|
const r = new BinaryReader(new Uint8Array([0x01]).buffer);
|
|
r.readByte();
|
|
expect(() => r.readByte()).toThrow(RangeError);
|
|
});
|
|
|
|
test("seek moves cursor", () => {
|
|
const r = new BinaryReader(new Uint8Array([10, 20, 30]).buffer);
|
|
r.seek(2);
|
|
expect(r.readByte()).toBe(30);
|
|
});
|
|
|
|
test("seek out of bounds throws", () => {
|
|
const r = new BinaryReader(new Uint8Array([1, 2]).buffer);
|
|
expect(() => r.seek(5)).toThrow(RangeError);
|
|
});
|
|
|
|
test("readUtf8 decodes ASCII", () => {
|
|
const str = "hello";
|
|
const enc = new TextEncoder().encode(str);
|
|
const r = new BinaryReader(enc.buffer);
|
|
expect(r.readUtf8(5)).toBe("hello");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MonoDisplayParser — static content
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("MonoDisplayParser — static", () => {
|
|
const parser = new MonoDisplayParser();
|
|
|
|
test("parses 2x2 static frame", () => {
|
|
const header = makeHeader(0, 2, 2);
|
|
const pixels = new Uint8Array([1, 0, 0, 1]); // diagonal
|
|
const buf = concat(header, pixels);
|
|
|
|
const result = parser.parse(buf) as StaticFrame;
|
|
expect(result.type).toBe("static");
|
|
expect(result.width).toBe(2);
|
|
expect(result.height).toBe(2);
|
|
expect(result.pixels[0]).toBe(1);
|
|
expect(result.pixels[3]).toBe(1);
|
|
});
|
|
|
|
test("rejects wrong magic", () => {
|
|
const bad = new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF, 1, 0, 4, 0, 4, 0]);
|
|
expect(() => parser.parse(bad.buffer)).toThrow(/magic/i);
|
|
});
|
|
|
|
test("rejects unsupported version", () => {
|
|
const buf = makeHeader(0, 1, 1);
|
|
buf[4] = 99; // version 99
|
|
expect(() => parser.parse(concat(buf, new Uint8Array([0])))).toThrow(/version/i);
|
|
});
|
|
|
|
test("rejects unknown content type", () => {
|
|
const buf = makeHeader(0xFF, 1, 1);
|
|
expect(() => parser.parse(concat(buf, new Uint8Array([0])))).toThrow(/content type/i);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MonoDisplayParser — animation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("MonoDisplayParser — animation", () => {
|
|
const parser = new MonoDisplayParser();
|
|
|
|
test("parses 2-frame animation", () => {
|
|
const header = makeHeader(1, 2, 2); // type=1 animation
|
|
const frameCount = u16le(2);
|
|
const frame1 = concat(u32le(100), new Uint8Array([1, 1, 1, 1]));
|
|
const frame2 = concat(u32le(200), new Uint8Array([0, 0, 0, 0]));
|
|
const buf = concat(header, frameCount, new Uint8Array(frame1), new Uint8Array(frame2));
|
|
|
|
const result = parser.parse(buf) as Animation;
|
|
expect(result.type).toBe("animation");
|
|
expect(result.frames.length).toBe(2);
|
|
expect(result.frames[0].durationMs).toBe(100);
|
|
expect(result.frames[1].durationMs).toBe(200);
|
|
});
|
|
|
|
test("empty animation (0 frames) is valid", () => {
|
|
const header = makeHeader(1, 4, 4);
|
|
const buf = concat(header, u16le(0));
|
|
const result = parser.parse(buf) as Animation;
|
|
expect(result.frames.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MonoDisplayParser — text
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("MonoDisplayParser — text", () => {
|
|
const parser = new MonoDisplayParser();
|
|
|
|
test("parses text content", () => {
|
|
const header = makeHeader(2, 0, 0); // w/h unused for text
|
|
const str = "HELLO";
|
|
const enc = new TextEncoder().encode(str);
|
|
const payload = concat(
|
|
u16le(enc.byteLength),
|
|
enc,
|
|
new Uint8Array([0]), // fontId
|
|
new Uint8Array([1]), // align = center
|
|
);
|
|
const buf = concat(header, new Uint8Array(payload));
|
|
|
|
const result = parser.parse(buf) as TextDisplay;
|
|
expect(result.type).toBe("text");
|
|
expect(result.text).toBe("HELLO");
|
|
expect(result.align).toBe(1);
|
|
expect(result.fontId).toBe(0);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MonoDisplayParser — scrolltext
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("MonoDisplayParser — scrolltext", () => {
|
|
const parser = new MonoDisplayParser();
|
|
|
|
test("parses scrolltext content", () => {
|
|
const header = makeHeader(3, 0, 0);
|
|
const str = "SCROLL ME";
|
|
const enc = new TextEncoder().encode(str);
|
|
const payload = concat(
|
|
u16le(enc.byteLength),
|
|
enc,
|
|
u16le(60), // speedPps = 60
|
|
new Uint8Array([0]), // direction = left
|
|
);
|
|
const buf = concat(header, new Uint8Array(payload));
|
|
|
|
const result = parser.parse(buf) as ScrollText;
|
|
expect(result.type).toBe("scrolltext");
|
|
expect(result.text).toBe("SCROLL ME");
|
|
expect(result.speedPps).toBe(60);
|
|
expect(result.direction).toBe(0);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// buildBinBuffer round-trip
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("buildBinBuffer", () => {
|
|
const parser = new MonoDisplayParser();
|
|
|
|
test("static round-trip", () => {
|
|
const pixels = new Uint8Array([0, 1, 1, 0]);
|
|
const original: StaticFrame = { type: "static", width: 2, height: 2, pixels };
|
|
const buf = buildBinBuffer(original);
|
|
const parsed = parser.parse(buf) as StaticFrame;
|
|
expect(parsed.type).toBe("static");
|
|
expect(parsed.width).toBe(2);
|
|
expect(parsed.height).toBe(2);
|
|
expect(Array.from(parsed.pixels)).toEqual([0, 1, 1, 0]);
|
|
});
|
|
|
|
test("non-static throws (not implemented yet)", () => {
|
|
const anim: Animation = { type: "animation", width: 1, height: 1, frames: [] };
|
|
expect(() => buildBinBuffer(anim)).toThrow(/not yet implemented/);
|
|
});
|
|
});
|
|
|