Abfahrtsanzeiger Display Basic Library
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.
 
 
 
libmonoformat/ts/test/library.test.ts

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