import { describe, expect, it } from "vitest"; import { addDays, berlinDay, daysBetween, formatDateTime, formatMonth, fromWallTime, monthKey, nextFullHour, nextOccurrence, toWallTime, wholeDayRange, } from "@/lib/dates"; // vitest.config.ts runs this with TZ=America/New_York, so none of these can // pass by accident on a machine that happens to be in Berlin. describe("the test environment", () => { it("is not in Berlin", () => { expect(Intl.DateTimeFormat().resolvedOptions().timeZone).toBe("America/New_York"); }); }); describe("fromWallTime / toWallTime", () => { it("treats wall times as Berlin in summer (UTC+2) and winter (UTC+1)", () => { expect(fromWallTime("2026-06-15", "19:00").toISOString()).toBe("2026-06-15T17:00:00.000Z"); expect(fromWallTime("2026-12-15", "19:00").toISOString()).toBe("2026-12-15T18:00:00.000Z"); }); it("round-trips", () => { expect(toWallTime(fromWallTime("2026-10-03", "07:05"))).toEqual({ date: "2026-10-03", time: "07:05" }); }); it("reads an instant back as Berlin, including across midnight", () => { // 23:30 UTC is already the next day in Berlin. expect(toWallTime(new Date("2026-06-15T23:30:00Z"))).toEqual({ date: "2026-06-16", time: "01:30" }); }); it("moves a time in the spring-forward gap to after it", () => { // 02:30 does not exist on 2026-03-29 in Berlin; it becomes 03:30 CEST. const instant = fromWallTime("2026-03-29", "02:30"); expect(instant.toISOString()).toBe("2026-03-29T01:30:00.000Z"); expect(toWallTime(instant)).toEqual({ date: "2026-03-29", time: "03:30" }); }); it("resolves a time that exists twice in autumn to the second (CET) one", () => { expect(fromWallTime("2026-10-25", "02:30").toISOString()).toBe("2026-10-25T01:30:00.000Z"); }); }); describe("day arithmetic", () => { it("adds days across month and year ends", () => { expect(addDays("2026-12-31", 1)).toBe("2027-01-01"); expect(addDays("2026-03-01", -1)).toBe("2026-02-28"); }); it("is not thrown off by the DST switch", () => { expect(addDays("2026-03-28", 2)).toBe("2026-03-30"); expect(daysBetween("2026-10-24", "2026-10-26")).toBe(2); }); it("reports the Berlin day", () => { expect(berlinDay(new Date("2026-06-15T22:30:00Z"))).toBe("2026-06-16"); }); }); describe("wholeDayRange", () => { it("spans 00:00 on the first day to 23:59 on the last, Berlin", () => { const { start, end } = wholeDayRange("2026-07-04", "2026-07-05"); expect(toWallTime(start)).toEqual({ date: "2026-07-04", time: "00:00" }); expect(toWallTime(end)).toEqual({ date: "2026-07-05", time: "23:59" }); }); }); describe("nextFullHour", () => { it("rounds up to the next hour in Berlin", () => { expect(nextFullHour(new Date("2026-06-15T17:20:00Z"))).toEqual({ date: "2026-06-15", time: "20:00" }); }); it("rolls over midnight", () => { expect(nextFullHour(new Date("2026-06-15T21:59:00Z"))).toEqual({ date: "2026-06-16", time: "00:00" }); }); }); describe("nextOccurrence", () => { const now = new Date("2026-09-27T10:00:00Z"); it("leaves a future start alone", () => { const start = fromWallTime("2026-10-01", "00:00"); expect(nextOccurrence(start, "YEARLY", now)).toBe(start); }); it("moves a birthday to this year when it is still ahead", () => { const start = fromWallTime("1990-11-03", "00:00"); expect(toWallTime(nextOccurrence(start, "YEARLY", now))).toEqual({ date: "2026-11-03", time: "00:00" }); }); it("moves a birthday to next year when it has passed", () => { const start = fromWallTime("1990-02-10", "00:00"); expect(toWallTime(nextOccurrence(start, "YEARLY", now)).date).toBe("2027-02-10"); }); it("counts a birthday today as current", () => { const start = fromWallTime("1990-09-27", "00:00"); expect(toWallTime(nextOccurrence(start, "YEARLY", now)).date).toBe("2026-09-27"); }); it("puts 29 February on 28 February in common years", () => { const start = fromWallTime("2000-02-29", "00:00"); expect(toWallTime(nextOccurrence(start, "YEARLY", now)).date).toBe("2027-02-28"); }); it("handles weekly and monthly series", () => { const monday = fromWallTime("2026-01-05", "19:30"); // a Monday expect(toWallTime(nextOccurrence(monday, "WEEKLY", now))).toEqual({ date: "2026-09-28", time: "19:30" }); const the31st = fromWallTime("2026-01-31", "18:00"); expect(toWallTime(nextOccurrence(the31st, "MONTHLY", now)).date).toBe("2026-09-30"); }); it("leaves unknown frequencies alone", () => { const start = fromWallTime("2020-01-01", "00:00"); expect(nextOccurrence(start, "SOMETIMES", now)).toBe(start); }); }); describe("formatting", () => { it("formats German, in Berlin time", () => { const instant = new Date("2026-10-03T17:00:00Z"); expect(formatDateTime(instant)).toBe("03.10.2026, 19:00"); expect(formatMonth(instant)).toBe("Oktober 2026"); expect(monthKey(new Date("2026-09-30T22:30:00Z"))).toBe("2026-10"); }); });