Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
|
|
import {
|
|
|
|
|
|
formatClockTime,
|
|
|
|
|
|
calculateTimeAfterPick,
|
|
|
|
|
|
getTimerColorClass,
|
2026-05-15 14:02:49 -07:00
|
|
|
|
parseDraftSpeed,
|
|
|
|
|
|
getInitialDraftSpeed,
|
|
|
|
|
|
CHESS_CLOCK_PRESETS,
|
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
|
|
|
|
} from "../draft-timer";
|
|
|
|
|
|
|
2026-05-15 14:02:49 -07:00
|
|
|
|
// ─── parseDraftSpeed ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe("parseDraftSpeed", () => {
|
|
|
|
|
|
describe("standard mode", () => {
|
|
|
|
|
|
it("returns the raw seconds for both fields", () => {
|
|
|
|
|
|
expect(parseDraftSpeed("120", "standard")).toEqual({ draftInitialTime: 120, draftIncrementTime: 120 });
|
|
|
|
|
|
expect(parseDraftSpeed("28800", "standard")).toEqual({ draftInitialTime: 28800, draftIncrementTime: 28800 });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("falls back to 90s for null or unparseable input", () => {
|
|
|
|
|
|
expect(parseDraftSpeed(null, "standard")).toEqual({ draftInitialTime: 90, draftIncrementTime: 90 });
|
|
|
|
|
|
expect(parseDraftSpeed("abc", "standard")).toEqual({ draftInitialTime: 90, draftIncrementTime: 90 });
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe("chess_clock mode — named presets", () => {
|
|
|
|
|
|
it.each(CHESS_CLOCK_PRESETS)("maps $value to bankSec=$bankSec / incrSec=$incrSec", ({ value, bankSec, incrSec }) => {
|
|
|
|
|
|
expect(parseDraftSpeed(value, "chess_clock")).toEqual({
|
|
|
|
|
|
draftInitialTime: bankSec,
|
|
|
|
|
|
draftIncrementTime: incrSec,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe("chess_clock mode — custom string", () => {
|
|
|
|
|
|
it("parses custom:{bank}:{incr} correctly", () => {
|
|
|
|
|
|
expect(parseDraftSpeed("custom:28800:1800", "chess_clock")).toEqual({
|
|
|
|
|
|
draftInitialTime: 28800,
|
|
|
|
|
|
draftIncrementTime: 1800,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe("chess_clock mode — fallback", () => {
|
|
|
|
|
|
it("returns standard preset values for null or unknown input", () => {
|
|
|
|
|
|
expect(parseDraftSpeed(null, "chess_clock")).toEqual({ draftInitialTime: 120, draftIncrementTime: 15 });
|
|
|
|
|
|
expect(parseDraftSpeed("unknown", "chess_clock")).toEqual({ draftInitialTime: 120, draftIncrementTime: 15 });
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── getInitialDraftSpeed ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe("getInitialDraftSpeed", () => {
|
|
|
|
|
|
it("returns increment seconds as string for standard mode", () => {
|
|
|
|
|
|
expect(getInitialDraftSpeed({ draftTimerMode: "standard", draftInitialTime: 120, draftIncrementTime: 120 })).toBe("120");
|
|
|
|
|
|
expect(getInitialDraftSpeed({ draftTimerMode: "standard", draftInitialTime: 28800, draftIncrementTime: 28800 })).toBe("28800");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("defaults to '90' for standard mode with missing increment", () => {
|
|
|
|
|
|
expect(getInitialDraftSpeed({ draftTimerMode: "standard", draftInitialTime: null, draftIncrementTime: null })).toBe("90");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it.each(CHESS_CLOCK_PRESETS)("recognises the $value preset from its DB values", ({ value, bankSec, incrSec }) => {
|
|
|
|
|
|
expect(getInitialDraftSpeed({ draftTimerMode: "chess_clock", draftInitialTime: bankSec, draftIncrementTime: incrSec })).toBe(value);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("returns custom:{bank}:{incr} for non-preset chess clock values", () => {
|
|
|
|
|
|
expect(getInitialDraftSpeed({ draftTimerMode: "chess_clock", draftInitialTime: 28800, draftIncrementTime: 1800 }))
|
|
|
|
|
|
.toBe("custom:28800:1800");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("defaults to 'standard' preset key when chess clock has no saved values", () => {
|
|
|
|
|
|
expect(getInitialDraftSpeed({ draftTimerMode: "chess_clock", draftInitialTime: null, draftIncrementTime: null })).toBe("standard");
|
|
|
|
|
|
expect(getInitialDraftSpeed(null)).toBe("standard");
|
|
|
|
|
|
expect(getInitialDraftSpeed(undefined)).toBe("standard");
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
|
|
|
|
// ─── formatClockTime ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe("formatClockTime", () => {
|
|
|
|
|
|
it("returns --:-- for undefined", () => {
|
|
|
|
|
|
expect(formatClockTime(undefined)).toBe("--:--");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("formats 0 seconds", () => {
|
|
|
|
|
|
expect(formatClockTime(0)).toBe("0:00");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("pads single-digit seconds", () => {
|
|
|
|
|
|
expect(formatClockTime(5)).toBe("0:05");
|
|
|
|
|
|
expect(formatClockTime(9)).toBe("0:09");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("formats seconds under a minute", () => {
|
|
|
|
|
|
expect(formatClockTime(30)).toBe("0:30");
|
|
|
|
|
|
expect(formatClockTime(59)).toBe("0:59");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("formats exactly one minute", () => {
|
|
|
|
|
|
expect(formatClockTime(60)).toBe("1:00");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("formats mixed minutes and seconds", () => {
|
|
|
|
|
|
expect(formatClockTime(90)).toBe("1:30");
|
|
|
|
|
|
expect(formatClockTime(125)).toBe("2:05");
|
|
|
|
|
|
expect(formatClockTime(150)).toBe("2:30");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("formats the default initial bank time", () => {
|
|
|
|
|
|
expect(formatClockTime(120)).toBe("2:00");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("formats hours when >= 3600 seconds", () => {
|
|
|
|
|
|
expect(formatClockTime(3600)).toBe("1:00:00");
|
|
|
|
|
|
expect(formatClockTime(3661)).toBe("1:01:01");
|
|
|
|
|
|
expect(formatClockTime(7322)).toBe("2:02:02");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("pads minutes when hours are present", () => {
|
|
|
|
|
|
expect(formatClockTime(3605)).toBe("1:00:05");
|
|
|
|
|
|
expect(formatClockTime(3660)).toBe("1:01:00");
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── calculateTimeAfterPick ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe("calculateTimeAfterPick", () => {
|
|
|
|
|
|
it("adds increment to a healthy bank", () => {
|
|
|
|
|
|
expect(calculateTimeAfterPick(90, 30)).toBe(120);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("adds increment when bank is exactly zero (timer expired before pick)", () => {
|
|
|
|
|
|
// Timer ran out but autopick fired — team still earns the increment
|
|
|
|
|
|
expect(calculateTimeAfterPick(0, 30)).toBe(30);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("adds increment to a depleted bank (barely positive)", () => {
|
|
|
|
|
|
expect(calculateTimeAfterPick(3, 30)).toBe(33);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("accumulates correctly across multiple quick picks", () => {
|
|
|
|
|
|
const increment = 30;
|
|
|
|
|
|
let bank = 120; // initialTime
|
|
|
|
|
|
|
|
|
|
|
|
// Round 1 — uses 10s, picks fast
|
|
|
|
|
|
bank -= 10;
|
|
|
|
|
|
bank = calculateTimeAfterPick(bank, increment); // 140
|
|
|
|
|
|
|
|
|
|
|
|
// Round 2 — uses 20s
|
|
|
|
|
|
bank -= 20;
|
|
|
|
|
|
bank = calculateTimeAfterPick(bank, increment); // 150
|
|
|
|
|
|
|
|
|
|
|
|
// Round 3 — uses 5s
|
|
|
|
|
|
bank -= 5;
|
|
|
|
|
|
bank = calculateTimeAfterPick(bank, increment); // 175
|
|
|
|
|
|
|
|
|
|
|
|
expect(bank).toBe(175);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("depletes over many slow picks", () => {
|
|
|
|
|
|
const increment = 30;
|
|
|
|
|
|
let bank = 120;
|
|
|
|
|
|
|
|
|
|
|
|
// Each round the team uses more time than the increment replenishes
|
|
|
|
|
|
for (let round = 0; round < 5; round++) {
|
|
|
|
|
|
bank -= 50; // uses 50s each turn (net loss of 20s per pick)
|
|
|
|
|
|
bank = calculateTimeAfterPick(bank, increment);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Started at 120, lost 20s net × 5 rounds = 20s remaining
|
|
|
|
|
|
expect(bank).toBe(20);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("works with a non-default increment value", () => {
|
|
|
|
|
|
expect(calculateTimeAfterPick(60, 15)).toBe(75);
|
|
|
|
|
|
expect(calculateTimeAfterPick(60, 60)).toBe(120);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── getTimerColorClass ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe("getTimerColorClass", () => {
|
|
|
|
|
|
it("returns muted for undefined (clock not running)", () => {
|
|
|
|
|
|
expect(getTimerColorClass(undefined)).toBe("text-muted-foreground");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("returns green for time well above the warning threshold (> 60s)", () => {
|
|
|
|
|
|
expect(getTimerColorClass(120)).toBe("text-emerald-400");
|
|
|
|
|
|
expect(getTimerColorClass(61)).toBe("text-emerald-400");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("returns amber at the 60-second boundary", () => {
|
|
|
|
|
|
// 60 is NOT > 60, so it falls to amber
|
|
|
|
|
|
expect(getTimerColorClass(60)).toBe("text-amber-accent");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("returns amber for time between 31–60s", () => {
|
|
|
|
|
|
expect(getTimerColorClass(45)).toBe("text-amber-accent");
|
|
|
|
|
|
expect(getTimerColorClass(31)).toBe("text-amber-accent");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("returns coral at the 30-second boundary", () => {
|
|
|
|
|
|
expect(getTimerColorClass(30)).toBe("text-coral-accent");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("returns coral for time between 11–30s", () => {
|
|
|
|
|
|
expect(getTimerColorClass(20)).toBe("text-coral-accent");
|
|
|
|
|
|
expect(getTimerColorClass(11)).toBe("text-coral-accent");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("returns coral+pulse at the 10-second boundary", () => {
|
|
|
|
|
|
expect(getTimerColorClass(10)).toContain("text-coral-accent");
|
|
|
|
|
|
expect(getTimerColorClass(10)).toContain("animate-pulse");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("returns coral+pulse for critical time (≤ 10s)", () => {
|
|
|
|
|
|
expect(getTimerColorClass(5)).toContain("animate-pulse");
|
|
|
|
|
|
expect(getTimerColorClass(1)).toContain("animate-pulse");
|
|
|
|
|
|
expect(getTimerColorClass(0)).toContain("animate-pulse");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("never returns animate-pulse above the critical threshold", () => {
|
|
|
|
|
|
expect(getTimerColorClass(11)).not.toContain("animate-pulse");
|
|
|
|
|
|
expect(getTimerColorClass(30)).not.toContain("animate-pulse");
|
|
|
|
|
|
expect(getTimerColorClass(120)).not.toContain("animate-pulse");
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Draft clock lifecycle scenarios ─────────────────────────────────────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// These tests document the intended chess-clock behaviour end-to-end and guard
|
|
|
|
|
|
// against the specific regressions we've hit:
|
|
|
|
|
|
//
|
|
|
|
|
|
// REGRESSION 1 — Timer reset: next team's bank was being reset to initialTime
|
|
|
|
|
|
// instead of left untouched.
|
|
|
|
|
|
// REGRESSION 2 — Double increment: increment was added both at pick-start AND
|
|
|
|
|
|
// after the pick, giving teams 2× the intended bonus.
|
|
|
|
|
|
|
|
|
|
|
|
describe("Draft clock lifecycle", () => {
|
|
|
|
|
|
const INITIAL_TIME = 120;
|
|
|
|
|
|
const INCREMENT = 30;
|
|
|
|
|
|
|
|
|
|
|
|
it("every team starts with exactly initialTime — no increment added upfront", () => {
|
|
|
|
|
|
// draft.start.ts sets timeRemaining: initialTime (not initialTime + increment)
|
|
|
|
|
|
const startingBank = INITIAL_TIME;
|
|
|
|
|
|
expect(startingBank).toBe(120);
|
|
|
|
|
|
expect(startingBank).not.toBe(INITIAL_TIME + INCREMENT);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("REGRESSION 1 — next team's bank is preserved when another team picks", () => {
|
|
|
|
|
|
// Team B has been on the clock and has 75s remaining.
|
|
|
|
|
|
// Team A (not team B) makes a pick.
|
|
|
|
|
|
// Team B's bank must NOT be touched at all.
|
|
|
|
|
|
const teamBBank = 75;
|
|
|
|
|
|
|
|
|
|
|
|
// Verify that calling calculateTimeAfterPick on team B WOULD change the value —
|
|
|
|
|
|
// this proves that a mutation is detectable and the correct behaviour is to skip it.
|
|
|
|
|
|
const incorrectlyUpdated = calculateTimeAfterPick(teamBBank, INCREMENT);
|
|
|
|
|
|
expect(incorrectlyUpdated).not.toBe(teamBBank); // 105 ≠ 75: mutation would be visible
|
|
|
|
|
|
|
|
|
|
|
|
// Correct behaviour: team B's bank is left unchanged (no calculateTimeAfterPick called)
|
|
|
|
|
|
expect(teamBBank).toBe(75);
|
|
|
|
|
|
expect(teamBBank).not.toBe(INITIAL_TIME); // 75 ≠ 120: not reset to initialTime (old bug)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("REGRESSION 2 — increment is added exactly once per pick (post-pick only)", () => {
|
|
|
|
|
|
// Team A is on the clock. They have 90s and use 30s before picking.
|
|
|
|
|
|
let teamABank = 90;
|
|
|
|
|
|
teamABank -= 30; // 60s left at pick time
|
|
|
|
|
|
|
|
|
|
|
|
// Post-pick reward: +increment once
|
|
|
|
|
|
teamABank = calculateTimeAfterPick(teamABank, INCREMENT);
|
|
|
|
|
|
|
|
|
|
|
|
expect(teamABank).toBe(90); // 60 + 30 = 90 (not 120, which would be double-increment)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("full two-team, three-round snake draft simulation", () => {
|
|
|
|
|
|
// Settings
|
|
|
|
|
|
const init = INITIAL_TIME; // 120s
|
|
|
|
|
|
const inc = INCREMENT; // 30s
|
|
|
|
|
|
|
|
|
|
|
|
let teamA = init; // 120
|
|
|
|
|
|
let teamB = init; // 120
|
|
|
|
|
|
|
|
|
|
|
|
// ── Round 1 ──────────────────────────
|
|
|
|
|
|
// Pick 1: Team A on clock, uses 20s
|
|
|
|
|
|
teamA -= 20; // 100
|
|
|
|
|
|
teamA = calculateTimeAfterPick(teamA, inc); // 130
|
|
|
|
|
|
// teamB untouched: 120
|
|
|
|
|
|
|
|
|
|
|
|
// Pick 2: Team B on clock, uses 60s
|
|
|
|
|
|
teamB -= 60; // 60
|
|
|
|
|
|
teamB = calculateTimeAfterPick(teamB, inc); // 90
|
|
|
|
|
|
// teamA untouched: 130
|
|
|
|
|
|
|
|
|
|
|
|
// ── Round 2 (reversed) ───────────────
|
|
|
|
|
|
// Pick 3: Team B on clock, uses 40s
|
|
|
|
|
|
teamB -= 40; // 50
|
|
|
|
|
|
teamB = calculateTimeAfterPick(teamB, inc); // 80
|
|
|
|
|
|
// teamA untouched: 130
|
|
|
|
|
|
|
|
|
|
|
|
// Pick 4: Team A on clock, uses 10s
|
|
|
|
|
|
teamA -= 10; // 120
|
|
|
|
|
|
teamA = calculateTimeAfterPick(teamA, inc); // 150
|
|
|
|
|
|
// teamB untouched: 80
|
|
|
|
|
|
|
|
|
|
|
|
// ── Round 3 ──────────────────────────
|
|
|
|
|
|
// Pick 5: Team A on clock, uses 80s
|
|
|
|
|
|
teamA -= 80; // 70
|
|
|
|
|
|
teamA = calculateTimeAfterPick(teamA, inc); // 100
|
|
|
|
|
|
// teamB untouched: 80
|
|
|
|
|
|
|
|
|
|
|
|
// Pick 6: Team B on clock, uses 75s
|
|
|
|
|
|
teamB -= 75; // 5
|
|
|
|
|
|
teamB = calculateTimeAfterPick(teamB, inc); // 35
|
|
|
|
|
|
// teamA untouched: 100
|
|
|
|
|
|
|
|
|
|
|
|
expect(teamA).toBe(100); // quick picker — healthy bank
|
|
|
|
|
|
expect(teamB).toBe(35); // slow picker — barely surviving
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("team that consistently runs out of time survives on increments alone", () => {
|
|
|
|
|
|
let bank = INITIAL_TIME; // 120
|
|
|
|
|
|
|
|
|
|
|
|
for (let round = 0; round < 4; round++) {
|
|
|
|
|
|
// Use the entire bank each round (timer hits 0 before pick fires)
|
|
|
|
|
|
bank = 0;
|
|
|
|
|
|
bank = calculateTimeAfterPick(bank, INCREMENT);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// After 4 rounds of running the clock dry, bank = just the increment
|
|
|
|
|
|
expect(bank).toBe(INCREMENT); // 30s — still alive, just barely
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|