Files
bookhoard/web/src/audio.test.ts
T
John O'Keefe a9430f3c06 feat(audio-player): end-time label toggles total / book-left / chapter-left (plan §10 web)
Clicking the end time cycles the three display modes, persisted per
device in localStorage (audio_time_display). Pure helpers (mode list,
normalize, cycle, endTimeLabel with 0-clamp and no-chapters fallback
to book remaining) + vitest; aria-label announces the current mode and
the next. Remaining values track the scrub cursor mid-drag. 140 unit
tests; 15 Playwright checks on KJV + Gardens.
2026-10-22 13:11:16 -04:00

142 lines
4.6 KiB
TypeScript
Raw 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, 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");
});
});