// ============================================================================= // 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/); }); });