import { describe, expect, it } from "vitest"; import { SPEEDS, SKIP_SECONDS, TIME_DISPLAY_MODES, cycleSkip, cycleTimeDisplay, endTimeLabel, normalizeAutoplay, normalizeSpeed, normalizeTimeDisplay, stepSpeed, } from "./audio"; describe("SPEEDS", () => { it("is the agreed list from the player plan (0.5 through 3)", () => { expect(SPEEDS).toEqual([0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3]); }); it("contains 1× exactly once and stays ascending", () => { expect(SPEEDS.filter((r) => r === 1)).toHaveLength(1); expect([...SPEEDS].sort((a, b) => a - b)).toEqual([...SPEEDS]); }); }); describe("normalizeSpeed", () => { it("passes through every listed rate", () => { for (const rate of SPEEDS) { expect(normalizeSpeed(String(rate))).toBe(rate); } }); it("falls back to 1× for null, garbage and out-of-list values", () => { expect(normalizeSpeed(null)).toBe(1); expect(normalizeSpeed("")).toBe(1); expect(normalizeSpeed("not-a-number")).toBe(1); expect(normalizeSpeed("0")).toBe(1); expect(normalizeSpeed("2.2")).toBe(1); expect(normalizeSpeed("10")).toBe(1); }); it("accepts a stale pre-plan entry of 0.75 or 2", () => { expect(normalizeSpeed("0.75")).toBe(0.75); expect(normalizeSpeed("2")).toBe(2); }); }); describe("stepSpeed", () => { it("steps forward and backward through the list", () => { expect(stepSpeed(1, 1)).toBe(1.25); expect(stepSpeed(1, -1)).toBe(0.75); expect(stepSpeed(0.5, 1)).toBe(0.75); expect(stepSpeed(2, 1)).toBe(2.5); }); it("clamps at both ends instead of wrapping", () => { expect(stepSpeed(0.5, -1)).toBe(0.5); expect(stepSpeed(3, 1)).toBe(3); }); it("anchors unknown currents at 1× before stepping", () => { expect(stepSpeed(2.2, 1)).toBe(1.25); expect(stepSpeed(2.2, -1)).toBe(0.75); }); }); describe("SKIP_SECONDS + cycleSkip", () => { it("is the agreed skip list", () => { expect(SKIP_SECONDS).toEqual([1, 5, 10, 15, 30]); }); it("cycles forward and backward, clamping at the ends", () => { expect(cycleSkip(10, 1)).toBe(15); expect(cycleSkip(10, -1)).toBe(5); expect(cycleSkip(1, -1)).toBe(1); expect(cycleSkip(30, 1)).toBe(30); }); it("anchors unknown currents at 10s before stepping", () => { expect(cycleSkip(7, 1)).toBe(15); expect(cycleSkip(7, -1)).toBe(5); }); }); describe("normalizeAutoplay", () => { it("is on only for explicitly-on values", () => { expect(normalizeAutoplay("1")).toBe(true); expect(normalizeAutoplay("true")).toBe(true); }); it("defaults to off for null, off, and garbage", () => { expect(normalizeAutoplay(null)).toBe(false); expect(normalizeAutoplay("0")).toBe(false); expect(normalizeAutoplay("")).toBe(false); expect(normalizeAutoplay("on")).toBe(false); expect(normalizeAutoplay("2")).toBe(false); }); }); describe("time display mode (§10 end-label toggle)", () => { it("cycles total → book remaining → chapter remaining → total", () => { expect(TIME_DISPLAY_MODES).toEqual(["total", "book-left", "chapter-left"]); expect(cycleTimeDisplay("total")).toBe("book-left"); expect(cycleTimeDisplay("book-left")).toBe("chapter-left"); expect(cycleTimeDisplay("chapter-left")).toBe("total"); }); it("normalizes persisted values, defaulting to total", () => { expect(normalizeTimeDisplay("total")).toBe("total"); expect(normalizeTimeDisplay("book-left")).toBe("book-left"); expect(normalizeTimeDisplay("chapter-left")).toBe("chapter-left"); expect(normalizeTimeDisplay(null)).toBe("total"); expect(normalizeTimeDisplay("")).toBe("total"); expect(normalizeTimeDisplay("remaining")).toBe("total"); }); it("renders total as the book duration", () => { // 93807s = 26:03:27 (Gardens-shaped) expect(endTimeLabel("total", 0, 93_807, null)).toBe("26:03:27"); }); it("renders book remaining, clamped at zero", () => { expect(endTimeLabel("book-left", 93_807 - 3_662, 93_807, null)).toBe("−1:01:02"); expect(endTimeLabel("book-left", 93_807, 93_807, null)).toBe("−0:00"); expect(endTimeLabel("book-left", 99_999, 93_807, null)).toBe("−0:00"); }); it("renders chapter remaining with minutes:seconds formatting", () => { // 842s left in the chapter → −14:02 expect(endTimeLabel("chapter-left", 5_000, 93_807, 5_842)).toBe("−14:02"); }); it("falls back to book remaining when the book has no chapters", () => { expect(endTimeLabel("chapter-left", 93_807 - 3_662, 93_807, null)).toBe("−1:01:02"); }); it("treats a chapter end in the past as zero remaining", () => { expect(endTimeLabel("chapter-left", 6_000, 93_807, 5_842)).toBe("−0:00"); }); });