Files
Voiced/src/pdf/engine/columns.test.ts
T
xavierk 92ba3d9590 Add bundled TTF fonts, page geometry, column allocator and render model
Static TTF fonts (IBM Plex Sans/Mono, Noto Devanagari/Kannada) with a
fontkit-generated manifest, a verifier and OFL licences. Every family
registers a normal and italic source so fallback stacks cannot throw.
Adds A4/Letter geometry with margin presets, an exact column allocator,
deterministic Indian-grouping money and date formatting, text sanitising
and a pure buildRenderModel.
2026-10-04 04:38:03 +05:30

102 lines
4.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { FILL, ColumnsError, cols, colsDetailed, pinned } from "./columns";
/** mulberry32, so failures reproduce. */
function prng(seed: number) {
return () => {
seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const sumCents = (w: number[], gap: number) =>
Math.round(w.reduce((a, b) => a + b, 0) * 100) + Math.round(gap * 100) * (w.length - 1);
describe("cols", () => {
it("gives the fill column the remainder", () => {
expect(cols(500, [{ fixed: 30 }, { fill: true }, { fixed: 60 }, { fixed: 80 }])).toEqual([30, 330, 60, 80]);
});
it("subtracts gaps before allocating", () => {
const w = cols(500, [{ fixed: 100 }, { fill: true }, { fixed: 100 }], 10);
expect(w).toEqual([100, 280, 100]);
});
it("allocates fractions of the space after gaps and puts the rounding on the fill", () => {
const w = cols(523.28, [{ frac: 1 / 3 }, { frac: 1 / 3 }, { fill: true }]);
expect(w.reduce((a, b) => a + b, 0)).toBeCloseTo(523.28, 10);
expect(w[0]).toBe(174.43);
expect(w[1]).toBe(174.43);
expect(w[2]).toBe(174.42);
});
it("without a fill column the last fraction absorbs the remainder", () => {
const w = cols(100, [{ frac: 0.333 }, { frac: 0.333 }, { frac: 0.334 }]);
expect(sumCents(w, 0)).toBe(10000);
});
it("sums exactly for many random inputs", () => {
const rnd = prng(20260904);
for (let i = 0; i < 2000; i++) {
const total = Math.round((400 + rnd() * 300) * 100) / 100;
const gap = Math.round(rnd() * 12 * 100) / 100;
const n = 2 + Math.floor(rnd() * 5);
const specs: Parameters<typeof cols>[1] = [];
const fillAt = Math.floor(rnd() * n);
for (let k = 0; k < n; k++) {
if (k === fillAt) specs.push({ fill: true });
else if (rnd() < 0.5) specs.push({ fixed: Math.round(rnd() * 30 * 100) / 100 });
else specs.push({ frac: Math.round(rnd() * 0.08 * 1000) / 1000 });
}
const w = cols(total, specs, gap);
expect(sumCents(w, gap)).toBe(Math.round(total * 100));
expect(w.every((x) => x >= 0)).toBe(true);
expect(w.every((x) => Math.abs(x * 100 - Math.round(x * 100)) < 1e-6)).toBe(true);
}
});
it("reports columns under their min so a Row can stack", () => {
const r = colsDetailed(300, [{ fixed: 170 }, { fill: true, min: 160 }], 10);
expect(r.widths).toEqual([170, 120]);
expect(r.ok).toBe(false);
expect(r.short).toEqual([1]);
const ok = colsDetailed(400, [{ fixed: 170 }, { fill: true, min: 160 }], 10);
expect(ok.ok).toBe(true);
expect(ok.short).toEqual([]);
expect(colsDetailed(300, [{ frac: 0.2, min: 80 }, { fill: true }]).short).toEqual([0]);
});
it("throws typed errors", () => {
const err = (fn: () => unknown) => {
try {
fn();
} catch (e) {
return e;
}
};
const a = err(() => cols(100, [{ fixed: 80 }, { fixed: 30 }, { fill: true }]));
expect(a).toBeInstanceOf(ColumnsError);
expect((a as ColumnsError).code).toBe("fixed-exceeds-total");
expect((err(() => cols(100, [{ fixed: 95 }, { fill: true }], 10)) as ColumnsError).code).toBe("fixed-exceeds-total");
expect((err(() => cols(100, [{ fill: true }, { fill: true }])) as ColumnsError).code).toBe("multiple-fill");
expect((err(() => cols(100, [{ frac: 1.5 }])) as ColumnsError).code).toBe("invalid-spec");
expect((err(() => cols(-1, [{ fill: true }])) as ColumnsError).code).toBe("invalid-spec");
expect((err(() => cols(100, [{ frac: 0.8 }, { frac: 0.8 }, { fill: true }])) as ColumnsError).code).toBe("fixed-exceeds-total");
});
it("handles a lone fill and exact-fit fixed columns", () => {
expect(cols(210, [{ fill: true }])).toEqual([210]);
expect(cols(100, [{ fixed: 40 }, { fill: true }, { fixed: 60 }])).toEqual([40, 0, 60]);
});
});
describe("style helpers", () => {
it("pinned fixes the box on every axis", () => {
expect(pinned(42)).toEqual({ width: 42, minWidth: 42, maxWidth: 42 });
});
it("FILL pairs flexGrow with flexBasis 0", () => {
expect(FILL).toEqual({ flexGrow: 1, flexBasis: 0 });
});
});