Files
NextChair/src/lib/time/__tests__/business-time.test.ts
T
Stefan Lazic 8b63cc66a9 Booking Engine
Architecture: Six focused services with the BookingService/SchedulingService overlap resolved by giving SchedulingService exactly one job, the transactional double-booking guard , while BookingService owns the create/reschedule/cancel workflow around it.

59 unit tests, 24 integration tests
2026-08-02 18:36:39 +01:00

99 lines
4.0 KiB
TypeScript

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}$/);
});
});