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