feat: update tests + imports

pull/1/head
flop 4 weeks ago
parent 6dc75094c3
commit 942d7dff70
  1. 2
      ts/src/file.ts
  2. 4
      ts/src/helper.ts
  3. 56
      ts/src/index.ts
  4. 2
      ts/src/parser.ts
  5. 3
      ts/src/renderer.ts
  6. 464
      ts/test/library.test.ts
  7. 2
      ts/tsconfig.json

@ -255,6 +255,6 @@ export async function loadBinFile(url: string): Promise<ArrayBuffer> {
* Build a single-element-always .bin buffer containing one Image2D element. * Build a single-element-always .bin buffer containing one Image2D element.
* Convenience for tests; for production use MonoDisplayFile directly. * Convenience for tests; for production use MonoDisplayFile directly.
*/ */
export function buildBinBuffer(el: Image2DElement): ArrayBuffer { export function buildBinBuffer(el: Image2DElement): Uint8Array {
return new MonoDisplayFile({ elements_always: [el] }).toBuffer(); return new MonoDisplayFile({ elements_always: [el] }).toBuffer();
} }

@ -11,7 +11,7 @@ export function packPixels(pixels: Uint8Array, width: number, height: number): U
const total = width * height; const total = width * height;
const packed = new Uint8Array((total + 7) >> 3); const packed = new Uint8Array((total + 7) >> 3);
for (let i = 0; i < total; i++) { for (let i = 0; i < total; i++) {
if (pixels[i]) packed[i >> 3] |= 1 << (i & 7); if (pixels[i]) packed[i >> 3] = (packed[i >> 3] ?? 0) | (1 << (i & 7));
} }
return packed; return packed;
} }
@ -23,7 +23,7 @@ export function unpackPixels(packed: Uint8Array, width: number, height: number):
const total = width * height; const total = width * height;
const pixels = new Uint8Array(total); const pixels = new Uint8Array(total);
for (let i = 0; i < total; i++) { for (let i = 0; i < total; i++) {
pixels[i] = (packed[i >> 3] >> (i & 7)) & 1; pixels[i] = ((packed[i >> 3] ?? 0) >> (i & 7)) & 1;
} }
return pixels; return pixels;
} }

@ -6,11 +6,7 @@
// //
// import { MonoDisplayDriver, loadBinFile } from "./index.ts"; // import { MonoDisplayDriver, loadBinFile } from "./index.ts";
// //
// PLAN: if the project splits into sub-packages (parser / renderer / utils),
// this file will aggregate them here so callers never need to update imports.
// ============================================================================= // =============================================================================
// ============================================================================= // =============================================================================
// MonoDisplay Library - binary format parser + canvas renderer // MonoDisplay Library - binary format parser + canvas renderer
// //
@ -54,49 +50,9 @@
// ============================================================================= // =============================================================================
export { export * from "./types";
// Driver (primary browser-facing API) export { BinaryReader, packPixels, unpackPixels, packedSize, pad32 } from "./helper";
MonoDisplayDriver, export { MonoDisplayParser, MONOFORMAT_MAGIC_HEADER } from "./parser";
export { MonoDisplayRenderer } from "./renderer";
// File builder — JSON descriptor → binary .bin buffer export { type MonoDisplayDriverOptions, MonoDisplayDriver } from "./driver";
MonoDisplayFile, export { MonoDisplayFile, loadBinFile, buildBinBuffer } from "./file";
ElementType,
// Parser — useful when you want to parse without rendering
MonoDisplayParser,
// Low-level binary reader — exposed so power users can parse custom sections
BinaryReader,
// Renderer — exposed for embedding into custom canvas setups
MonoDisplayRenderer,
// Convenience helpers
loadBinFile,
buildBinBuffer,
} from "./library";
export type {
// File builder types
MonoDisplayFileDescriptor,
DisplayElement,
Image2DElement,
AnimationFrame,
AnimationElement,
TextElement,
ScrollTextElement,
// Union type for all display content variants
DisplayContent,
DisplayContentType,
// Concrete content types
StaticFrame,
AnimFrame,
Animation,
TextDisplay,
ScrollText,
// Driver options bag
MonoDisplayDriverOptions,
} from "./library";

@ -4,7 +4,7 @@
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
import { packedSize, packPixels, pad32, unpackPixels, BinaryReader } from "./helper"; import { packedSize, packPixels, pad32, unpackPixels, BinaryReader } from "./helper";
import { type MonoFormatFile } from "./library"; import { type MonoFormatFile } from "./types";
import { ElementType, SectionType, type MonoFormatAnimation, type MonoFormatClippedText, type MonoFormatCustomFont, type MonoFormatElement, type MonoFormatElementsAlways, type MonoFormatElementsTimespan, type MonoFormatHScroll, type MonoFormatHScrollText, type MonoFormatImage2D, type MonoFormatLine, type MonoFormatPixelImage, type MonoFormatSection, type MonoFormatSectionFlags, type MonoFormatVScroll } from "./types"; import { ElementType, SectionType, type MonoFormatAnimation, type MonoFormatClippedText, type MonoFormatCustomFont, type MonoFormatElement, type MonoFormatElementsAlways, type MonoFormatElementsTimespan, type MonoFormatHScroll, type MonoFormatHScrollText, type MonoFormatImage2D, type MonoFormatLine, type MonoFormatPixelImage, type MonoFormatSection, type MonoFormatSectionFlags, type MonoFormatVScroll } from "./types";
// Magic 0xAF 0x7E 0x2B 0x63 read as uint32 LE // Magic 0xAF 0x7E 0x2B 0x63 read as uint32 LE

@ -14,6 +14,7 @@ import {
type MonoFormatPixelImage, type MonoFormatPixelImage,
type MonoFormatVScroll type MonoFormatVScroll
} from "./types.js"; } from "./types.js";
import { type MonoDisplayDriverOptions } from "./driver";
/** /**
* Renders a MonoFormatFile onto an HTMLCanvasElement. * Renders a MonoFormatFile onto an HTMLCanvasElement.
* Uses setInterval at 1000/fps ms per tick. All element state (animation * Uses setInterval at 1000/fps ms per tick. All element state (animation
@ -151,7 +152,7 @@ export class MonoDisplayRenderer {
let state = this.animState.get(id); let state = this.animState.get(id);
if (!state) { state = { frame: 0, counter: 0 }; this.animState.set(id, state); } if (!state) { state = { frame: 0, counter: 0 }; this.animState.set(id, state); }
this.#blitImage(el.frames[state.frame], el.xOffset, el.yOffset); this.#blitImage(el.frames[state.frame]!, el.xOffset, el.yOffset);
state.counter++; state.counter++;
if (state.counter > el.updateInterval) { if (state.counter > el.updateInterval) {

@ -2,9 +2,6 @@
// library.test.ts — bun test suite for MonoDisplay library // library.test.ts — bun test suite for MonoDisplay library
// //
// run: bun test // run: bun test
//
// Tests cover BinaryReader primitives and MonoDisplayParser with the real
// binary format: magic 0xAF7E2B63, section-based layout.
// ============================================================================= // =============================================================================
import { test, expect, describe } from "bun:test"; import { test, expect, describe } from "bun:test";
@ -13,12 +10,12 @@ import {
MonoDisplayParser, MonoDisplayParser,
MonoDisplayFile, MonoDisplayFile,
ElementType, ElementType,
SectionType,
buildBinBuffer, buildBinBuffer,
type StaticFrame, type Image2DElement,
type Animation, type MonoFormatElementsAlways,
type TextDisplay, type MonoFormatImage2D,
type ScrollText, } from "../src/index";
} from "../src/library";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Low-level buffer helpers // Low-level buffer helpers
@ -28,7 +25,9 @@ import {
const MAGIC_BYTES = new Uint8Array([0xAF, 0x7E, 0x2B, 0x63]); const MAGIC_BYTES = new Uint8Array([0xAF, 0x7E, 0x2B, 0x63]);
function concat(...parts: (Uint8Array | ArrayBuffer)[]): ArrayBuffer { function concat(...parts: (Uint8Array | ArrayBuffer)[]): ArrayBuffer {
const arrays = parts.map(p => p instanceof ArrayBuffer ? new Uint8Array(p) : p); 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 total = arrays.reduce((s, a) => s + a.byteLength, 0);
const out = new Uint8Array(total); const out = new Uint8Array(total);
let off = 0; let off = 0;
@ -41,25 +40,6 @@ function u16le(n: number) { const b = new Uint8Array(2); new DataView(b.buffer).
function u24le(n: number) { return new Uint8Array([n & 0xFF, (n >> 8) & 0xFF, (n >> 16) & 0xFF]); } 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; } function u32le(n: number) { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, n, true); return b; }
/**
* Build a well-formed file buffer with a single section.
* sectionType: 0x01=static 0x02=animation 0x03=text 0x04=scrolltext
* sectionData: raw section payload (after section header)
*/
function makeFile(sectionType: number, sectionData: Uint8Array): ArrayBuffer {
const fileHeader = concat(
MAGIC_BYTES, // magic
u32le(1), // version = 1
u16le(1), // numSections = 1
u16le(0), // reserved = 0
);
const sectionHeader = concat(
u8(sectionType),
u24le(sectionData.byteLength),
);
return concat(fileHeader, sectionHeader, sectionData);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// BinaryReader // BinaryReader
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -159,273 +139,227 @@ describe("MonoDisplayParser — header", () => {
const buf = concat(MAGIC_BYTES, u32le(2), u16le(0), u16le(0)); const buf = concat(MAGIC_BYTES, u32le(2), u16le(0), u16le(0));
expect(() => parser.parse(buf)).toThrow(/version/i); expect(() => parser.parse(buf)).toThrow(/version/i);
}); });
test("rejects 0 sections", () => {
const buf = concat(MAGIC_BYTES, u32le(1), u16le(0), u16le(0)); // 0 sections
expect(() => parser.parse(buf)).toThrow(/0 section/i);
});
test("skips unknown section type, throws when no recognised section follows", () => {
// Section type 0xFF is unknown
const unknownData = new Uint8Array(4);
const fileHeader = concat(MAGIC_BYTES, u32le(1), u16le(1), u16le(0));
const secHeader = concat(u8(0xFF), u24le(unknownData.byteLength));
const buf = concat(fileHeader, secHeader, unknownData);
expect(() => parser.parse(buf)).toThrow(/no recognised/i);
});
});
// ---------------------------------------------------------------------------
// MonoDisplayParser — static section
// ---------------------------------------------------------------------------
describe("MonoDisplayParser — static", () => {
const parser = new MonoDisplayParser();
test("parses 2×2 static frame", () => {
const data = concat(u16le(2), u16le(2), new Uint8Array([1, 0, 0, 1]));
const buf = makeFile(0x01, new Uint8Array(data));
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);
});
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// MonoDisplayParser — animation section // MonoDisplayParser — Image2D round-trip
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe("MonoDisplayParser — animation", () => { describe("MonoDisplayParser — Image2D round-trip", () => {
const parser = new MonoDisplayParser(); const parser = new MonoDisplayParser();
test("parses 2-frame animation", () => { /**
const pixels = new Uint8Array(4); // 2×2 * Build a minimal valid file buffer containing one ElementsAlways section
const data = concat( * with one Image2D element.
u16le(2), u16le(2), // width, height *
u16le(2), // frameCount * File header (12 bytes):
u32le(100), pixels, // frame 1: 100ms, all-off * magic u32le + version u32le + numSections u16le + reserved u16le
u32le(200), new Uint8Array([1,1,1,1]), // frame 2: 200ms, all-on *
); * Section header (4 bytes):
const buf = makeFile(0x02, new Uint8Array(data)); * type u8 + size u24le (INCLUDES 4-byte header)
*
const result = parser.parse(buf) as Animation; * ElementsAlways section data:
expect(result.type).toBe("animation"); * flags u16le + numElements u16le
expect(result.frames.length).toBe(2); *
expect(result.frames[0].durationMs).toBe(100); * Image2D element (12 fixed bytes + packed pixel data):
expect(result.frames[1].durationMs).toBe(200); * type u16le + xOffset u16le + yOffset u16le + width u16le + height u16le + reserved u16le
}); * + packed 1bpp pixel data + padding to 4-byte boundary
*/
test("empty animation (0 frames) is valid", () => { function makeImage2DFile(
const data = concat(u16le(4), u16le(4), u16le(0)); width: number,
const buf = makeFile(0x02, new Uint8Array(data)); height: number,
const result = parser.parse(buf) as Animation; pixels: Uint8Array,
expect(result.frames.length).toBe(0); xOffset: number = 0,
}); yOffset: number = 0,
}); ): ArrayBuffer {
// Pack pixels to 1bpp
// --------------------------------------------------------------------------- const total = width * height;
// MonoDisplayParser — text section const packedLen = (total + 7) >> 3;
// --------------------------------------------------------------------------- const packed = new Uint8Array(packedLen);
for (let i = 0; i < total; i++) {
describe("MonoDisplayParser — text", () => { if (pixels[i]) packed[i >> 3] = (packed[i >> 3] ?? 0) | (1 << (i & 7));
const parser = new MonoDisplayParser(); }
// Padding to 4-byte boundary for element (12 fixed bytes + packed data)
test("parses text with align=center", () => { const elementDataLen = 12 + packedLen;
const enc = new TextEncoder().encode("HELLO"); const padLen = (4 - (elementDataLen & 3)) & 3;
const data = concat(u16le(enc.byteLength), enc, u8(0), u8(1)); // fontId=0 align=1 const padding = new Uint8Array(padLen);
const buf = makeFile(0x03, new Uint8Array(data));
// Image2D element bytes
const result = parser.parse(buf) as TextDisplay; const element = new Uint8Array(concat(
expect(result.type).toBe("text"); u16le(1), // type = Image2D
expect(result.text).toBe("HELLO"); u16le(xOffset),
expect(result.align).toBe(1); u16le(yOffset),
expect(result.fontId).toBe(0); 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
));
test("handles multi-byte UTF-8 text", () => { return concat(fileHeader, sectionHeader, sectionData);
const str = "日本語"; }
const enc = new TextEncoder().encode(str);
const data = concat(u16le(enc.byteLength), enc, u8(0), u8(0));
const buf = makeFile(0x03, new Uint8Array(data));
const result = parser.parse(buf) as TextDisplay; test("sections[0].sectionType === SectionType.ElementsAlways", () => {
expect(result.text).toBe(str); 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);
}); });
});
// ---------------------------------------------------------------------------
// MonoDisplayParser — scrolltext section
// ---------------------------------------------------------------------------
describe("MonoDisplayParser — scrolltext", () => {
const parser = new MonoDisplayParser();
test("parses scrolltext left-direction", () => { test("elements[0].type === ElementType.Image2D", () => {
const enc = new TextEncoder().encode("SCROLL ME"); const pixels = new Uint8Array([1, 0, 0, 1]);
const data = concat(u16le(enc.byteLength), enc, u16le(60), u8(0)); const buf = makeImage2DFile(2, 2, pixels);
const buf = makeFile(0x04, new Uint8Array(data)); const result = parser.parse(buf);
const section = result.sections[0] as MonoFormatElementsAlways;
const result = parser.parse(buf) as ScrollText; expect(section.elements.length).toBe(1);
expect(result.type).toBe("scrolltext"); expect(section.elements[0]!.type).toBe(ElementType.Image2D);
expect(result.text).toBe("SCROLL ME");
expect(result.speedPps).toBe(60);
expect(result.direction).toBe(0);
}); });
test("handles multi-codepoint text incl. emoji", () => { test("correct dimensions after parse", () => {
const str = "TICKER 🚀 日本語"; const pixels = new Uint8Array([0, 1, 1, 0]);
const enc = new TextEncoder().encode(str); const buf = makeImage2DFile(2, 2, pixels);
const data = concat(u16le(enc.byteLength), enc, u16le(80), u8(1)); const result = parser.parse(buf);
const buf = makeFile(0x04, new Uint8Array(data)); const section = result.sections[0] as MonoFormatElementsAlways;
const el = section.elements[0] as MonoFormatImage2D;
const result = parser.parse(buf) as ScrollText; expect(el.image.width).toBe(2);
expect(result.text).toBe(str); expect(el.image.height).toBe(2);
expect(result.direction).toBe(1); });
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);
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// MonoDisplayParser — unknown section skip // MonoDisplayFile
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe("MonoDisplayParser — section skip", () => { describe("MonoDisplayFile", () => {
const parser = new MonoDisplayParser(); const parser = new MonoDisplayParser();
test("skips unknown section, parses second known section", () => { test("toBuffer() produces parseable output for Image2D element", () => {
// File with 2 sections: 0xFF (unknown) then 0x01 (static 2×2) const pixels = new Uint8Array([1, 0, 1, 0]);
const staticData = new Uint8Array(concat(u16le(2), u16le(2), new Uint8Array([0,1,1,0]))); const el: Image2DElement = { type: ElementType.Image2D, pixels, width: 2, height: 2 };
const unknownData = new Uint8Array(4).fill(0xAA); const file = new MonoDisplayFile({ elements_always: [el] });
const buf = file.toBuffer();
const fileHeader = concat(MAGIC_BYTES, u32le(1), u16le(2), u16le(0)); const result = parser.parse(buf);
const sec1 = concat(u8(0xFF), u24le(unknownData.byteLength), unknownData); expect(result.sections.length).toBeGreaterThan(0);
const sec2 = concat(u8(0x01), u24le(staticData.byteLength), staticData); const section = result.sections[0] as MonoFormatElementsAlways;
const buf = concat(fileHeader, sec1, sec2); expect(section.sectionType).toBe(SectionType.ElementsAlways);
expect(section.elements.length).toBe(1);
const result = parser.parse(buf) as StaticFrame; const parsed = section.elements[0] as MonoFormatImage2D;
expect(result.type).toBe("static"); expect(parsed.type).toBe(ElementType.Image2D);
expect(result.pixels[1]).toBe(1); 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 round-trip // buildBinBuffer
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe("buildBinBuffer", () => { describe("buildBinBuffer", () => {
const parser = new MonoDisplayParser(); const parser = new MonoDisplayParser();
test("static round-trip", () => { test("builds a parseable Uint8Array from Image2DElement", () => {
const pixels = new Uint8Array([0, 1, 1, 0]); const pixels = new Uint8Array([0, 1, 1, 0]);
const original: StaticFrame = { type: "static", width: 2, height: 2, pixels }; const el: Image2DElement = { type: ElementType.Image2D, pixels, width: 2, height: 2 };
const buf = buildBinBuffer(original); const buf = buildBinBuffer(el);
const parsed = parser.parse(buf) as StaticFrame; expect(buf).toBeInstanceOf(Uint8Array);
expect(parsed.type).toBe("static"); const result = parser.parse(buf);
expect(parsed.width).toBe(2); expect(result.sections.length).toBeGreaterThan(0);
expect(parsed.height).toBe(2); const section = result.sections[0] as MonoFormatElementsAlways;
expect(Array.from(parsed.pixels)).toEqual([0, 1, 1, 0]); const parsed = section.elements[0] as MonoFormatImage2D;
}); expect(Array.from(parsed.image.pixels)).toEqual([0, 1, 1, 0]);
});
test("non-static throws (not implemented yet)", () => {
const anim: Animation = { type: "animation", width: 1, height: 1, frames: [] }; test("pixel data round-trips correctly", () => {
expect(() => buildBinBuffer(anim)).toThrow(/not yet implemented/); 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;
// MonoDisplayFile — JSON descriptor builder const parsed = section.elements[0] as MonoFormatImage2D;
// --------------------------------------------------------------------------- expect(parsed.image.width).toBe(8);
expect(parsed.image.height).toBe(1);
describe("MonoDisplayFile", () => { expect(Array.from(parsed.image.pixels)).toEqual([1, 0, 0, 0, 0, 0, 0, 1]);
const parser = new MonoDisplayParser();
test("single Image2D fills full display", () => {
const pixels = new Uint8Array(4 * 2).fill(1); // 4×2 all-on
const file = new MonoDisplayFile({
width: 4, height: 2,
elements_always: [{ type: ElementType.Image2D, pixels, width: 4, height: 2 }],
});
const result = parser.parse(file.toBuffer()) as StaticFrame;
expect(result.type).toBe("static");
expect(result.width).toBe(4);
expect(result.height).toBe(2);
expect(result.pixels.every(p => p === 1)).toBe(true);
});
test("xoffset / yoffset positions element", () => {
// 6×2 display, 2×2 all-on block placed at xoffset=2
const block = new Uint8Array(4).fill(1);
const file = new MonoDisplayFile({
width: 6, height: 2,
elements_always: [{ type: ElementType.Image2D, pixels: block, width: 2, height: 2, xoffset: 2 }],
});
const result = parser.parse(file.toBuffer()) as StaticFrame;
// cols 0,1 off — cols 2,3 on — cols 4,5 off
expect(result.pixels[0]).toBe(0); // (0,0) off
expect(result.pixels[2]).toBe(1); // (2,0) on
expect(result.pixels[3]).toBe(1); // (3,0) on
expect(result.pixels[4]).toBe(0); // (4,0) off
});
test("two overlapping elements OR-blend", () => {
// 4×1 display: element A sets pixels [0,1,0,0], element B sets [0,0,0,1]
const a = new Uint8Array([0, 1, 0, 0]);
const b = new Uint8Array([0, 0, 0, 1]);
const file = new MonoDisplayFile({
width: 4, height: 1,
elements_always: [
{ type: ElementType.Image2D, pixels: a, width: 4, height: 1 },
{ type: ElementType.Image2D, pixels: b, width: 4, height: 1 },
],
});
const result = parser.parse(file.toBuffer()) as StaticFrame;
expect(Array.from(result.pixels)).toEqual([0, 1, 0, 1]);
});
test("element clipped at right/bottom edge", () => {
// 4×2 display, 2×2 block placed at xoffset=3 — only 1 col fits
const block = new Uint8Array(4).fill(1);
const file = new MonoDisplayFile({
width: 4, height: 2,
elements_always: [{ type: ElementType.Image2D, pixels: block, width: 2, height: 2, xoffset: 3 }],
});
const result = parser.parse(file.toBuffer()) as StaticFrame;
// col 3 on, col 4+ clipped
expect(result.pixels[3]).toBe(1); // (3,0) on
expect(result.width).toBe(4); // canvas didn't grow
});
test("size inferred from element bounding box when omitted", () => {
const pixels = new Uint8Array(3 * 2).fill(1);
const file = new MonoDisplayFile({
elements_always: [{ type: ElementType.Image2D, pixels, width: 3, height: 2, xoffset: 1, yoffset: 1 }],
});
const result = parser.parse(file.toBuffer()) as StaticFrame;
expect(result.width).toBe(4); // 1 + 3
expect(result.height).toBe(3); // 1 + 2
});
test("empty descriptor produces 160×80 blank static frame", () => {
const file = new MonoDisplayFile({});
const result = parser.parse(file.toBuffer()) as StaticFrame;
expect(result.width).toBe(160);
expect(result.height).toBe(80);
expect(result.pixels.every(p => p === 0)).toBe(true);
});
test("toBuffer round-trips through parser", () => {
const pixels = new Uint8Array(120 * 80);
for (let i = 0; i < pixels.length; i++) pixels[i] = i % 2;
const file = new MonoDisplayFile({
width: 160, height: 80,
elements_always: [{ type: ElementType.Image2D, pixels, width: 120, height: 80, xoffset: 20 }],
});
const result = parser.parse(file.toBuffer()) as StaticFrame;
expect(result.width).toBe(160);
expect(result.pixels[20]).toBe(0); // first pixel of element (i=0 → 0)
expect(result.pixels[21]).toBe(1); // second pixel (i=1 → 1)
}); });
}); });

@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
// Environment setup & latest features // Environment setup & latest features
"lib": ["ESNext"], "lib": ["ESNext", "DOM"],
"target": "ESNext", "target": "ESNext",
"module": "Preserve", "module": "Preserve",
"moduleDetection": "force", "moduleDetection": "force",

Loading…
Cancel
Save