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.
365 lines
13 KiB
365 lines
13 KiB
// =============================================================================
|
|
// library.test.ts — bun test suite for MonoDisplay library
|
|
//
|
|
// run: bun test
|
|
// =============================================================================
|
|
|
|
import { test, expect, describe } from "bun:test";
|
|
import {
|
|
BinaryReader,
|
|
MonoDisplayParser,
|
|
MonoDisplayFile,
|
|
ElementType,
|
|
SectionType,
|
|
buildBinBuffer,
|
|
type Image2DElement,
|
|
type MonoFormatElementsAlways,
|
|
type MonoFormatImage2D,
|
|
} from "../src/index";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Low-level buffer helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Real magic: 0xAF 0x7E 0x2B 0x63
|
|
const MAGIC_BYTES = new Uint8Array([0xAF, 0x7E, 0x2B, 0x63]);
|
|
|
|
function concat(...parts: (Uint8Array | ArrayBuffer)[]): ArrayBuffer {
|
|
const arrays = parts.map((p: Uint8Array | ArrayBuffer) =>
|
|
p instanceof ArrayBuffer ? new Uint8Array(p) : p
|
|
);
|
|
const total = arrays.reduce((s, a) => s + a.byteLength, 0);
|
|
const out = new Uint8Array(total);
|
|
let off = 0;
|
|
for (const a of arrays) { out.set(a, off); off += a.byteLength; }
|
|
return out.buffer;
|
|
}
|
|
|
|
function u8(n: number) { return new Uint8Array([n]); }
|
|
function u16le(n: number) { const b = new Uint8Array(2); new DataView(b.buffer).setUint16(0, n, true); return b; }
|
|
function u24le(n: number) { return new Uint8Array([n & 0xFF, (n >> 8) & 0xFF, (n >> 16) & 0xFF]); }
|
|
function u32le(n: number) { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, n, true); return b; }
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// BinaryReader
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("BinaryReader", () => {
|
|
test("readByte", () => {
|
|
const r = new BinaryReader(new Uint8Array([0xAB]).buffer);
|
|
expect(r.readByte()).toBe(0xAB);
|
|
expect(r.remaining).toBe(0);
|
|
});
|
|
|
|
test("readUint16 LE", () => {
|
|
const r = new BinaryReader(new Uint8Array([0x34, 0x12]).buffer);
|
|
expect(r.readUint16()).toBe(0x1234);
|
|
});
|
|
|
|
test("readShort alias", () => {
|
|
const r = new BinaryReader(new Uint8Array([0x01, 0x00]).buffer);
|
|
expect(r.readShort()).toBe(1);
|
|
});
|
|
|
|
test("readUint24 LE", () => {
|
|
// 0x010203 LE → bytes [0x03, 0x02, 0x01]
|
|
const r = new BinaryReader(new Uint8Array([0x03, 0x02, 0x01]).buffer);
|
|
expect(r.readUint24()).toBe(0x010203);
|
|
});
|
|
|
|
test("readUint32 LE", () => {
|
|
const r = new BinaryReader(new Uint8Array([0x78, 0x56, 0x34, 0x12]).buffer);
|
|
expect(r.readUint32()).toBe(0x12345678);
|
|
});
|
|
|
|
test("readUint64 LE", () => {
|
|
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 through mixed reads", () => {
|
|
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); // only 3 remain
|
|
});
|
|
|
|
test("RangeError on exhaustion", () => {
|
|
const r = new BinaryReader(new Uint8Array([0x01]).buffer);
|
|
r.readByte();
|
|
expect(() => r.readByte()).toThrow(RangeError);
|
|
});
|
|
|
|
test("seek", () => {
|
|
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 ASCII", () => {
|
|
const enc = new TextEncoder().encode("hello");
|
|
const r = new BinaryReader(enc.buffer);
|
|
expect(r.readUtf8(5)).toBe("hello");
|
|
});
|
|
|
|
test("readUtf8 multi-byte codepoints", () => {
|
|
const str = "日本語🚀";
|
|
const enc = new TextEncoder().encode(str);
|
|
const r = new BinaryReader(enc.buffer);
|
|
expect(r.readUtf8(enc.byteLength)).toBe(str);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MonoDisplayParser — header validation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("MonoDisplayParser — header", () => {
|
|
const parser = new MonoDisplayParser();
|
|
|
|
test("rejects wrong magic", () => {
|
|
const bad = new Uint8Array(16).fill(0);
|
|
bad[0] = 0xDE; bad[1] = 0xAD; bad[2] = 0xBE; bad[3] = 0xEF;
|
|
expect(() => parser.parse(bad.buffer)).toThrow(/magic/i);
|
|
});
|
|
|
|
test("rejects version 0 (illegal)", () => {
|
|
const buf = concat(MAGIC_BYTES, u32le(0), u16le(0), u16le(0));
|
|
expect(() => parser.parse(buf)).toThrow(/version/i);
|
|
});
|
|
|
|
test("rejects version > 1 (reserved)", () => {
|
|
const buf = concat(MAGIC_BYTES, u32le(2), u16le(0), u16le(0));
|
|
expect(() => parser.parse(buf)).toThrow(/version/i);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MonoDisplayParser — Image2D round-trip
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("MonoDisplayParser — Image2D round-trip", () => {
|
|
const parser = new MonoDisplayParser();
|
|
|
|
/**
|
|
* Build a minimal valid file buffer containing one ElementsAlways section
|
|
* with one Image2D element.
|
|
*
|
|
* File header (12 bytes):
|
|
* magic u32le + version u32le + numSections u16le + reserved u16le
|
|
*
|
|
* Section header (4 bytes):
|
|
* type u8 + size u24le (INCLUDES 4-byte header)
|
|
*
|
|
* ElementsAlways section data:
|
|
* flags u16le + numElements u16le
|
|
*
|
|
* Image2D element (12 fixed bytes + packed pixel data):
|
|
* type u16le + xOffset u16le + yOffset u16le + width u16le + height u16le + reserved u16le
|
|
* + packed 1bpp pixel data + padding to 4-byte boundary
|
|
*/
|
|
function makeImage2DFile(
|
|
width: number,
|
|
height: number,
|
|
pixels: Uint8Array,
|
|
xOffset: number = 0,
|
|
yOffset: number = 0,
|
|
): ArrayBuffer {
|
|
// Pack pixels to 1bpp
|
|
const total = width * height;
|
|
const packedLen = (total + 7) >> 3;
|
|
const packed = new Uint8Array(packedLen);
|
|
for (let i = 0; i < total; i++) {
|
|
if (pixels[i]) packed[i >> 3] = (packed[i >> 3] ?? 0) | (1 << (i & 7));
|
|
}
|
|
// Padding to 4-byte boundary for element (12 fixed bytes + packed data)
|
|
const elementDataLen = 12 + packedLen;
|
|
const padLen = (4 - (elementDataLen & 3)) & 3;
|
|
const padding = new Uint8Array(padLen);
|
|
|
|
// Image2D element bytes
|
|
const element = new Uint8Array(concat(
|
|
u16le(1), // type = Image2D
|
|
u16le(xOffset),
|
|
u16le(yOffset),
|
|
u16le(width),
|
|
u16le(height),
|
|
u16le(0), // reserved
|
|
packed,
|
|
padding,
|
|
));
|
|
|
|
// Section data: flags + numElements + element
|
|
const sectionData = new Uint8Array(concat(
|
|
u16le(0), // flags = 0
|
|
u16le(1), // numElements = 1
|
|
element,
|
|
));
|
|
|
|
// Section header: type u8 + size u24le (4 header bytes + data)
|
|
const sectionSize = 4 + sectionData.byteLength;
|
|
const sectionHeader = new Uint8Array(concat(
|
|
u8(1), // SectionType.ElementsAlways = 1
|
|
u24le(sectionSize),
|
|
));
|
|
|
|
// File header
|
|
const fileHeader = new Uint8Array(concat(
|
|
MAGIC_BYTES,
|
|
u32le(1), // version = 1
|
|
u16le(1), // numSections = 1
|
|
u16le(0), // reserved
|
|
));
|
|
|
|
return concat(fileHeader, sectionHeader, sectionData);
|
|
}
|
|
|
|
test("sections[0].sectionType === SectionType.ElementsAlways", () => {
|
|
const pixels = new Uint8Array([1, 0, 0, 1]);
|
|
const buf = makeImage2DFile(2, 2, pixels);
|
|
const result = parser.parse(buf);
|
|
expect(result.sections.length).toBe(1);
|
|
expect(result.sections[0]!.sectionType).toBe(SectionType.ElementsAlways);
|
|
});
|
|
|
|
test("elements[0].type === ElementType.Image2D", () => {
|
|
const pixels = new Uint8Array([1, 0, 0, 1]);
|
|
const buf = makeImage2DFile(2, 2, pixels);
|
|
const result = parser.parse(buf);
|
|
const section = result.sections[0] as MonoFormatElementsAlways;
|
|
expect(section.elements.length).toBe(1);
|
|
expect(section.elements[0]!.type).toBe(ElementType.Image2D);
|
|
});
|
|
|
|
test("correct dimensions after parse", () => {
|
|
const pixels = new Uint8Array([0, 1, 1, 0]);
|
|
const buf = makeImage2DFile(2, 2, pixels);
|
|
const result = parser.parse(buf);
|
|
const section = result.sections[0] as MonoFormatElementsAlways;
|
|
const el = section.elements[0] as MonoFormatImage2D;
|
|
expect(el.image.width).toBe(2);
|
|
expect(el.image.height).toBe(2);
|
|
});
|
|
|
|
test("correct pixel data after round-trip", () => {
|
|
// pixels: [1, 0, 0, 1] → top-left and bottom-right are on
|
|
const pixels = new Uint8Array([1, 0, 0, 1]);
|
|
const buf = makeImage2DFile(2, 2, pixels);
|
|
const result = parser.parse(buf);
|
|
const section = result.sections[0] as MonoFormatElementsAlways;
|
|
const el = section.elements[0] as MonoFormatImage2D;
|
|
expect(Array.from(el.image.pixels)).toEqual([1, 0, 0, 1]);
|
|
});
|
|
|
|
test("xOffset and yOffset are preserved", () => {
|
|
const pixels = new Uint8Array([1, 1, 1, 1]);
|
|
const buf = makeImage2DFile(2, 2, pixels, 5, 3);
|
|
const result = parser.parse(buf);
|
|
const section = result.sections[0] as MonoFormatElementsAlways;
|
|
const el = section.elements[0] as MonoFormatImage2D;
|
|
expect(el.xOffset).toBe(5);
|
|
expect(el.yOffset).toBe(3);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MonoDisplayFile
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("MonoDisplayFile", () => {
|
|
const parser = new MonoDisplayParser();
|
|
|
|
test("toBuffer() produces parseable output for Image2D element", () => {
|
|
const pixels = new Uint8Array([1, 0, 1, 0]);
|
|
const el: Image2DElement = { type: ElementType.Image2D, pixels, width: 2, height: 2 };
|
|
const file = new MonoDisplayFile({ elements_always: [el] });
|
|
const buf = file.toBuffer();
|
|
const result = parser.parse(buf);
|
|
expect(result.sections.length).toBeGreaterThan(0);
|
|
const section = result.sections[0] as MonoFormatElementsAlways;
|
|
expect(section.sectionType).toBe(SectionType.ElementsAlways);
|
|
expect(section.elements.length).toBe(1);
|
|
const parsed = section.elements[0] as MonoFormatImage2D;
|
|
expect(parsed.type).toBe(ElementType.Image2D);
|
|
expect(Array.from(parsed.image.pixels)).toEqual([1, 0, 1, 0]);
|
|
});
|
|
|
|
test("xOffset/yOffset are round-tripped", () => {
|
|
const pixels = new Uint8Array([1, 1, 1, 1]);
|
|
const el: Image2DElement = {
|
|
type: ElementType.Image2D,
|
|
pixels,
|
|
width: 2,
|
|
height: 2,
|
|
xOffset: 10,
|
|
yOffset: 7,
|
|
};
|
|
const file = new MonoDisplayFile({ elements_always: [el] });
|
|
const buf = file.toBuffer();
|
|
const result = parser.parse(buf);
|
|
const section = result.sections[0] as MonoFormatElementsAlways;
|
|
const parsed = section.elements[0] as MonoFormatImage2D;
|
|
expect(parsed.xOffset).toBe(10);
|
|
expect(parsed.yOffset).toBe(7);
|
|
});
|
|
|
|
test("flags.drawFront default is applied", () => {
|
|
const pixels = new Uint8Array([0]);
|
|
const el: Image2DElement = { type: ElementType.Image2D, pixels, width: 1, height: 1 };
|
|
const file = new MonoDisplayFile({ elements_always: [el] });
|
|
const buf = file.toBuffer();
|
|
const result = parser.parse(buf);
|
|
const section = result.sections[0] as MonoFormatElementsAlways;
|
|
// Default flags should have drawFront = true (flags byte bit 0 = 1)
|
|
expect(section.flags.drawFront).toBe(true);
|
|
});
|
|
|
|
test("empty elements_always array is valid and produces parseable output", () => {
|
|
const file = new MonoDisplayFile({ elements_always: [] });
|
|
const buf = file.toBuffer();
|
|
const result = parser.parse(buf);
|
|
expect(result.sections.length).toBeGreaterThan(0);
|
|
const section = result.sections[0] as MonoFormatElementsAlways;
|
|
expect(section.sectionType).toBe(SectionType.ElementsAlways);
|
|
expect(section.elements.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// buildBinBuffer
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("buildBinBuffer", () => {
|
|
const parser = new MonoDisplayParser();
|
|
|
|
test("builds a parseable Uint8Array from Image2DElement", () => {
|
|
const pixels = new Uint8Array([0, 1, 1, 0]);
|
|
const el: Image2DElement = { type: ElementType.Image2D, pixels, width: 2, height: 2 };
|
|
const buf = buildBinBuffer(el);
|
|
expect(buf).toBeInstanceOf(Uint8Array);
|
|
const result = parser.parse(buf);
|
|
expect(result.sections.length).toBeGreaterThan(0);
|
|
const section = result.sections[0] as MonoFormatElementsAlways;
|
|
const parsed = section.elements[0] as MonoFormatImage2D;
|
|
expect(Array.from(parsed.image.pixels)).toEqual([0, 1, 1, 0]);
|
|
});
|
|
|
|
test("pixel data round-trips correctly", () => {
|
|
const pixels = new Uint8Array([1, 0, 0, 0, 0, 0, 0, 1]); // 8 pixels, corners on
|
|
const el: Image2DElement = { type: ElementType.Image2D, pixels, width: 8, height: 1 };
|
|
const buf = buildBinBuffer(el);
|
|
const result = parser.parse(buf);
|
|
const section = result.sections[0] as MonoFormatElementsAlways;
|
|
const parsed = section.elements[0] as MonoFormatImage2D;
|
|
expect(parsed.image.width).toBe(8);
|
|
expect(parsed.image.height).toBe(1);
|
|
expect(Array.from(parsed.image.pixels)).toEqual([1, 0, 0, 0, 0, 0, 0, 1]);
|
|
});
|
|
});
|
|
|