import { describe, expect, it } from "vitest"; import { Weekday } from "@/generated/prisma/enums"; import { addDays, calendarDateOf, compareCalendarDates, eachCalendarDate, isAfterCalendarDate, isBeforeCalendarDate, minutesFromMidnight, todayIn, wallClockToInstant, weekdayOf, } from "../business-time"; describe("weekdayOf", () => { it("maps calendar dates to the correct Weekday", () => { // 2026-08-03 is a Monday. expect(weekdayOf("2026-08-03")).toBe(Weekday.MONDAY); expect(weekdayOf("2026-08-04")).toBe(Weekday.TUESDAY); expect(weekdayOf("2026-08-08")).toBe(Weekday.SATURDAY); expect(weekdayOf("2026-08-09")).toBe(Weekday.SUNDAY); }); }); describe("wallClockToInstant / minutesFromMidnight round-trip", () => { it("round-trips a plain wall-clock time in a fixed-offset zone", () => { const instant = wallClockToInstant("2026-01-15", 9 * 60 + 30, "Europe/London"); expect(minutesFromMidnight(instant, "Europe/London")).toBe(9 * 60 + 30); expect(calendarDateOf(instant, "Europe/London")).toBe("2026-01-15"); }); it("produces the documented UTC instant for a known GMT (winter) date", () => { // 2026-01-15 is outside BST — Europe/London is UTC+0. const instant = wallClockToInstant("2026-01-15", 10 * 60, "Europe/London"); expect(instant.toISOString()).toBe("2026-01-15T10:00:00.000Z"); }); }); describe("DST correctness (Europe/London BST transition, last Sunday of March 2026)", () => { it("the same wall-clock time yields UTC instants shifted by the DST offset change", () => { // Before the 2026-03-29 transition: Europe/London is GMT (UTC+0). const beforeTransition = wallClockToInstant("2026-03-28", 10 * 60, "Europe/London"); expect(beforeTransition.toISOString()).toBe("2026-03-28T10:00:00.000Z"); // After the transition: Europe/London is BST (UTC+1). const afterTransition = wallClockToInstant("2026-03-30", 10 * 60, "Europe/London"); expect(afterTransition.toISOString()).toBe("2026-03-30T09:00:00.000Z"); // A naive "always UTC+0" implementation would put these exactly 48h // apart; DST-correct math puts them 47h apart because a wall-clock // hour was skipped. const diffHours = (afterTransition.getTime() - beforeTransition.getTime()) / 3_600_000; expect(diffHours).toBe(47); }); it("minutesFromMidnight recovers the same local wall-clock time on both sides of the transition", () => { const before = wallClockToInstant("2026-03-28", 10 * 60, "Europe/London"); const after = wallClockToInstant("2026-03-30", 10 * 60, "Europe/London"); expect(minutesFromMidnight(before, "Europe/London")).toBe(10 * 60); expect(minutesFromMidnight(after, "Europe/London")).toBe(10 * 60); }); }); describe("calendar date arithmetic", () => { it("addDays is pure calendar-date math, independent of any zone", () => { expect(addDays("2026-02-27", 1)).toBe("2026-02-28"); expect(addDays("2026-02-28", 1)).toBe("2026-03-01"); // 2026 is not a leap year expect(addDays("2026-12-31", 1)).toBe("2027-01-01"); }); it("compares calendar dates correctly", () => { expect(compareCalendarDates("2026-01-01", "2026-01-02")).toBeLessThan(0); expect(compareCalendarDates("2026-01-02", "2026-01-01")).toBeGreaterThan(0); expect(compareCalendarDates("2026-01-01", "2026-01-01")).toBe(0); expect(isBeforeCalendarDate("2026-01-01", "2026-01-02")).toBe(true); expect(isAfterCalendarDate("2026-01-02", "2026-01-01")).toBe(true); }); it("eachCalendarDate yields an inclusive range and nothing when from > to", () => { expect([...eachCalendarDate("2026-01-01", "2026-01-03")]).toEqual([ "2026-01-01", "2026-01-02", "2026-01-03", ]); expect([...eachCalendarDate("2026-01-03", "2026-01-01")]).toEqual([]); }); it("eachCalendarDate guards against absurdly large ranges", () => { expect(() => [...eachCalendarDate("2026-01-01", "2030-01-01")]).toThrow(); }); }); describe("todayIn", () => { it("returns a well-formed calendar date", () => { expect(todayIn("Europe/London")).toMatch(/^\d{4}-\d{2}-\d{2}$/); }); });