import { formatBytes, formatClock, formatCount, formatDuration, formatMicros, formatPercent, formatRate, formatTime, } from "@/lib/format"; test("formatTime renders unix seconds in the given locale and zone", () => { // 2024-01-01T00:00:00Z; ICU emits U+202F before AM/PM in recent Node. expect(formatTime(1704067200, "en-US", "UTC").replace(/\u202f/g, " ")).toBe("Jan 1, 2024, 12:00:00 AM"); }); test("formatClock states the time of day alone, for a stamp read against now", () => { expect(formatClock(Date.UTC(2026, 0, 1, 14, 5) / 1000, "en-GB", "UTC")).toBe("14:05"); expect(formatClock(Date.UTC(2026, 0, 1, 9, 30) / 1000, "en-GB", "UTC")).toBe("09:30"); // No date: the caller places it against now, and a date would be noise. expect(formatClock(Date.UTC(2026, 0, 1, 14, 5) / 1000, "en-GB", "UTC")).not.toMatch(/2026/); }); test("formatCount groups thousands", () => { expect(formatCount(0)).toBe("0"); expect(formatCount(999)).toBe("999"); expect(formatCount(18432)).toBe("18,432"); expect(formatCount(1_234_567)).toBe("1,234,567"); }); test("formatPercent always carries two decimals, rounded half up", () => { expect(formatPercent(0)).toBe("0.00%"); expect(formatPercent(1)).toBe("100.00%"); expect(formatPercent(0.1544)).toBe("15.44%"); expect(formatPercent(0.154)).toBe("15.40%"); // A stored 15.435 sits just under the half; it still rounds up. expect(formatPercent(0.15435)).toBe("15.44%"); expect(formatPercent(0.15434)).toBe("15.43%"); expect(formatPercent(0.005)).toBe("0.50%"); expect(formatPercent(0.00005)).toBe("0.01%"); expect(formatPercent(0.00004)).toBe("0.00%"); }); test("formatRate carries one decimal, rounded half up", () => { expect(formatRate(12.84)).toBe("12.8"); expect(formatRate(12.85)).toBe("12.9"); expect(formatRate(5)).toBe("5.0"); expect(formatRate(0)).toBe("0.0"); }); test("formatBytes humanizes with binary units", () => { expect(formatBytes(0)).toBe("0 B"); expect(formatBytes(1023)).toBe("1023 B"); expect(formatBytes(1024)).toBe("1.0 KiB"); expect(formatBytes(1536)).toBe("1.5 KiB"); expect(formatBytes(5 * 1024 * 1024)).toBe("5.0 MiB"); expect(formatBytes(3 * 1024 * 1024 * 1024)).toBe("3.0 GiB"); expect(formatBytes(2 * 1024 ** 4)).toBe("2.0 TiB"); }); test("formatDuration is the two largest nonzero units, unpadded", () => { expect(formatDuration(45)).toBe("45s"); expect(formatDuration(60)).toBe("1m"); expect(formatDuration(725)).toBe("12m 5s"); expect(formatDuration(3600)).toBe("1h"); expect(formatDuration(3600 * 4 + 60 * 12 + 30)).toBe("4h 12m"); expect(formatDuration(86400 * 6 + 3600 * 4)).toBe("6d 4h"); // The two largest that are nonzero, whatever sits between them. expect(formatDuration(86400 * 6 + 5)).toBe("6d 5s"); expect(formatDuration(86400 * 6)).toBe("6d"); expect(formatDuration(86400 * 400)).toBe("400d"); // Never "06d 4h" or "6d 04h". expect(formatDuration(86400 * 6 + 3600 * 4)).not.toMatch(/0\d/); }); test("formatDuration reads under a second as such, skew included", () => { expect(formatDuration(0)).toBe("<1s"); expect(formatDuration(0.9)).toBe("<1s"); expect(formatDuration(-5)).toBe("<1s"); }); test("formatMicros renders milliseconds with one decimal", () => { expect(formatMicros(0)).toBe("0.0 ms"); expect(formatMicros(1234)).toBe("1.2 ms"); expect(formatMicros(999)).toBe("1.0 ms"); expect(formatMicros(2_500_000)).toBe("2500.0 ms"); }); /** * The contract's teeth, as far as a text scan can bite: no source outside this * module calls a number-formatting API. A bare `{count}` in JSX is invisible to * this scan and is caught in review; the scan closes the door on the four ways * a competing rule would be written. `datetime-local` zero-padding in * `activity/datetime.ts` is a wire grammar, not a display rule, and is the one * allowed `padStart`. Identifiers, configured values and preset labels are not * quantities at all and are exempt by the header rule, so the scan never sees * them. */ test("no source file outside format.ts calls a number-formatting API", () => { const sources = import.meta.glob("../**/*.{ts,tsx}", { query: "?raw", import: "default", eager: true }); const offenders: string[] = []; for (const [path, text] of Object.entries(sources)) { if (/\.test\.tsx?$/.test(path) || /(^|\/)format\.ts$/.test(path)) continue; for (const pattern of [/\.toFixed\(/, /\.toLocaleString\(/, /Intl\.NumberFormat/]) { if (pattern.test(text)) offenders.push(`${path}: ${pattern.source}`); } if (/\.padStart\(/.test(text) && !path.endsWith("/activity/datetime.ts")) offenders.push(`${path}: padStart`); } expect(Object.keys(sources).length).toBeGreaterThan(50); expect(offenders).toEqual([]); });