brackt/app/lib/__tests__/overnight-pause.test.ts

148 lines
5.9 KiB
TypeScript
Raw Permalink Normal View History

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:0007: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:0018: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:0007: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());
});
});