import { describe, it, expect } from "vitest"; import { isInOvernightWindow, getOvernightResumeUTC } from "../overnight-pause"; // Helper to build a Date at a specific local time in a timezone. // We use a known UTC offset approach: format a UTC date, check what local time // it produces, then shift until we hit the desired time. function dateAtLocalTime( timezone: string, year: number, month: number, // 1-indexed day: number, hour: number, minute: number ): Date { // Use a binary-search-free approach: construct approximate UTC, then correct const approxUtc = Date.UTC(year, month - 1, day, hour, minute); const probe = new Date(approxUtc); const fmt = new Intl.DateTimeFormat("en-US", { timeZone: timezone, hour: "2-digit", minute: "2-digit", hour12: false, }); const parts = fmt.formatToParts(probe); const localHour = parseInt(parts.find((p) => p.type === "hour")?.value ?? "0", 10); const localMin = parseInt(parts.find((p) => p.type === "minute")?.value ?? "0", 10); const h = localHour === 24 ? 0 : localHour; const diffMs = ((hour * 60 + minute) - (h * 60 + localMin)) * 60 * 1000; return new Date(approxUtc + diffMs); } // ─── isInOvernightWindow ────────────────────────────────────────────────────── describe("isInOvernightWindow — midnight-spanning window (23:00–07:00)", () => { const tz = "America/New_York"; const start = "23:00"; const end = "07:00"; it("returns true just after window starts (23:30)", () => { expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 15, 23, 30))).toBe(true); }); it("returns true at exact window start (23:00)", () => { expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 15, 23, 0))).toBe(true); }); it("returns true past midnight (01:00)", () => { expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 16, 1, 0))).toBe(true); }); it("returns true just before window ends (06:59)", () => { expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 16, 6, 59))).toBe(true); }); it("returns false exactly at window end (07:00)", () => { expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 16, 7, 0))).toBe(false); }); it("returns false in the middle of the day (14:00)", () => { expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 15, 14, 0))).toBe(false); }); it("returns false just before window starts (22:59)", () => { expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 15, 22, 59))).toBe(false); }); }); describe("isInOvernightWindow — same-day window (10:00–18:00)", () => { const tz = "America/Chicago"; const start = "10:00"; const end = "18:00"; it("returns true at 14:00", () => { expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 15, 14, 0))).toBe(true); }); it("returns true at exact start (10:00)", () => { expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 15, 10, 0))).toBe(true); }); it("returns false at exact end (18:00)", () => { expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 15, 18, 0))).toBe(false); }); it("returns false at 09:59", () => { expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 15, 9, 59))).toBe(false); }); it("returns false at 20:00", () => { expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 15, 20, 0))).toBe(false); }); }); describe("isInOvernightWindow — different IANA timezones", () => { it("works for Europe/London at 00:30 in a midnight-spanning window", () => { const d = dateAtLocalTime("Europe/London", 2025, 6, 15, 0, 30); expect(isInOvernightWindow("Europe/London", "23:00", "07:00", d)).toBe(true); }); it("works for Asia/Tokyo — 23:30 is in window", () => { const d = dateAtLocalTime("Asia/Tokyo", 2025, 6, 15, 23, 30); expect(isInOvernightWindow("Asia/Tokyo", "23:00", "07:00", d)).toBe(true); }); it("works for Asia/Tokyo — 14:00 is not in window", () => { const d = dateAtLocalTime("Asia/Tokyo", 2025, 6, 15, 14, 0); expect(isInOvernightWindow("Asia/Tokyo", "23:00", "07:00", d)).toBe(false); }); }); // ─── getOvernightResumeUTC ──────────────────────────────────────────────────── describe("getOvernightResumeUTC", () => { it("returns a future date when called during the overnight window", () => { const tz = "America/New_York"; // 2:00 AM ET — inside the 23:00–07:00 window const now = dateAtLocalTime(tz, 2025, 6, 16, 2, 0); const resume = getOvernightResumeUTC(tz, "07:00", now); expect(resume.getTime()).toBeGreaterThan(now.getTime()); }); it("returns a date that represents 07:00 in target timezone", () => { const tz = "America/Chicago"; const now = dateAtLocalTime(tz, 2025, 6, 16, 1, 0); const resume = getOvernightResumeUTC(tz, "07:00", now); // Verify it shows as 07:00 in Chicago const fmt = new Intl.DateTimeFormat("en-US", { timeZone: tz, hour: "2-digit", minute: "2-digit", hour12: false, }); const parts = fmt.formatToParts(resume); const h = parseInt(parts.find((p) => p.type === "hour")?.value ?? "0", 10); const m = parseInt(parts.find((p) => p.type === "minute")?.value ?? "0", 10); expect(h).toBe(7); expect(m).toBe(0); }); it("returns tomorrow's end time when current time is after window end (daytime)", () => { const tz = "America/New_York"; // 14:00 ET — outside the window, should return tomorrow 07:00 const now = dateAtLocalTime(tz, 2025, 6, 16, 14, 0); const resume = getOvernightResumeUTC(tz, "07:00", now); expect(resume.getTime()).toBeGreaterThan(now.getTime()); }); });