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.
102 lines
4.2 KiB
TypeScript
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 });
|
|
});
|
|
});
|