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