1
0
Fork 0
polaris-task-force/tests/int/session-countdown.int.spec.ts
Z8MB1E 962064a5a4 feat(session): add pre-expiry warning banner with extend button
- SessionExpiryBanner: live countdown, warn 15min early, one-shot toast, Extend button
- countdown.ts: pure countdown helpers (timeUntilExpiry, sessionExpiryPhase, formatCountdown)
- useSessionExpired: added unlock handler for SESSION_EXTENDED_EVENT
- useGameTick: exported SESSION_EXTENDED_EVENT for session extension
- unit tests: 14/14 pass for countdown helpers
- e2e test: banner renders with countdown + extend button
2026-09-23 15:02:10 -04:00

91 lines
2.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
SESSION_AUTO_CLOSE_MS,
SESSION_WARN_THRESHOLD_MS,
formatCountdown,
sessionExpiryPhase,
timeUntilExpiry,
} from "@/lib/session/countdown";
describe("timeUntilExpiry", () => {
it("returns sessionExp - now", () => {
expect(timeUntilExpiry(1_800_000_000, 1_700_000_000)).toBe(100_000_000);
});
it("defaults now to Date.now()", () => {
const now = Date.now();
const result = timeUntilExpiry(now + 5_000);
expect(result).toBeGreaterThanOrEqual(4_900);
expect(result).toBeLessThanOrEqual(5_000);
});
it("returns null when there is no known expiry", () => {
expect(timeUntilExpiry(null)).toBeNull();
expect(timeUntilExpiry(undefined)).toBeNull();
});
it("returns null for a non-finite expiry", () => {
expect(timeUntilExpiry(Number.NaN)).toBeNull();
expect(timeUntilExpiry(Number.POSITIVE_INFINITY)).toBeNull();
});
it("allows a negative remaining time (already expired)", () => {
expect(timeUntilExpiry(100, 200)).toBe(-100);
});
});
describe("sessionExpiryPhase", () => {
const warn = SESSION_WARN_THRESHOLD_MS; // 15 min
const close = SESSION_AUTO_CLOSE_MS; // 30s
it("returns idle when remaining exceeds the warn window", () => {
expect(sessionExpiryPhase(warn + 1)).toBe("idle");
expect(sessionExpiryPhase(null)).toBe("idle");
});
it("returns warn inside the window (close < remaining <= warn)", () => {
expect(sessionExpiryPhase(warn)).toBe("warn");
expect(sessionExpiryPhase(close + 1)).toBe("warn");
expect(sessionExpiryPhase(warn / 2)).toBe("warn");
});
it("returns close at or below the close threshold", () => {
expect(sessionExpiryPhase(close)).toBe("close");
expect(sessionExpiryPhase(0)).toBe("close");
expect(sessionExpiryPhase(-1)).toBe("close");
});
it("honours custom thresholds", () => {
expect(sessionExpiryPhase(10 * 60_000, 15 * 60_000, 5 * 60_000)).toBe("warn");
expect(sessionExpiryPhase(3 * 60_000, 15 * 60_000, 5 * 60_000)).toBe("close");
});
});
describe("formatCountdown", () => {
it("formats under an hour as MM:SS", () => {
expect(formatCountdown(0)).toBe("00:00");
expect(formatCountdown(59)).toBe("00:59");
expect(formatCountdown(60)).toBe("01:00");
expect(formatCountdown(90)).toBe("01:30");
expect(formatCountdown(599)).toBe("09:59");
});
it("formats hours as H:MM:SS", () => {
expect(formatCountdown(3600)).toBe("1:00:00");
expect(formatCountdown(3725)).toBe("1:02:05");
});
it("rounds up to the next whole second", () => {
expect(formatCountdown(0.1)).toBe("00:01");
expect(formatCountdown(59.4)).toBe("01:00");
});
it("never goes negative", () => {
expect(formatCountdown(-5)).toBe("00:00");
});
it("handles non-finite input", () => {
expect(formatCountdown(Number.NaN)).toBe("00:00");
expect(formatCountdown(Number.POSITIVE_INFINITY)).toBe("00:00");
});
});