brackt/app/lib/__tests__/overnight-pause.test.ts
Chris Parsons dbc23f14ce
Add overnight pause for draft timers (#335)
* Add overnight pause feature for draft timers

Protects players from having their pick timer expire while asleep.
Admins configure a nightly window (league-wide or per-user timezone);
the timer freezes during that window while autodraft can still fire.

Fixes #66

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix TS error: guard getUserDisplayName against undefined user

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 22:31:52 -07:00

147 lines
5.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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