import { describe, it, expect } from "vitest"; import { stripHeightFor, type StripHeightInput } from "./alphabetStrip"; /** * Regression: the A-Z jump strip ran under the mini player, so the tail of the * alphabet could not be tapped. * * The strip used to size itself against `window.innerHeight` minus a hardcoded * guess at the bottom bars' height (5rem / 7rem / 11rem, chosen by platform and * whether the mini player was showing). Those bars stopped being fixed overlays * when BottomUi became an in-flow flex sibling below the scroller, so the guess * has no relationship to the real stack — and it is short on any device with a * navigation/gesture bar, because `--safe-bottom` is padded *inside* BottomUi. * * The invariant every case below asserts: the strip must end at or above the * scroller's own bottom edge, which is exactly the top of the mini player. * * TRACES: UR-007 | DR-007 | UT-235, UT-236, UT-237 */ /** 800px-tall phone viewport; the library scroller starts 120px down. */ const VIEWPORT = 800; const STRIP_TOP = 120; /** Measured heights of the real bottom UI, in CSS px. */ const NAV = 77; // BottomNav: py-2 + icon 24 + gap 4 + label 16 + py-2, + 1px border const MINI = 69; // MiniPlayer: 4px progress bar + 48px artwork row + py-2, + 1px border const REMOTE_ROW = 32; // "Playing on " banner, remote mode only const GESTURE_BAR = 48; // --safe-bottom on a 3-button nav device function bounds(bottomUiHeight: number): StripHeightInput { return { stripTop: STRIP_TOP, scrollerBottom: VIEWPORT - bottomUiHeight, viewportHeight: VIEWPORT, }; } describe("stripHeightFor", () => { it("keeps the last letter above the bottom nav when nothing is playing", () => { const input = bounds(NAV + GESTURE_BAR); const bottom = STRIP_TOP + stripHeightFor(input); expect(bottom).toBeLessThanOrEqual(input.scrollerBottom!); }); it("keeps the last letter above the mini player while audio plays", () => { const input = bounds(MINI + NAV + GESTURE_BAR); const bottom = STRIP_TOP + stripHeightFor(input); expect(bottom).toBeLessThanOrEqual(input.scrollerBottom!); }); it("survives the taller mini player of remote mode", () => { const input = bounds(REMOTE_ROW + MINI + NAV + GESTURE_BAR); const bottom = STRIP_TOP + stripHeightFor(input); expect(bottom).toBeLessThanOrEqual(input.scrollerBottom!); }); it("still fills the space it does have, rather than stopping short", () => { const input = bounds(MINI + NAV + GESTURE_BAR); const available = input.scrollerBottom! - STRIP_TOP; // Within one letter's worth of the space available (letters are ~16px). expect(stripHeightFor(input)).toBeGreaterThan(available - 16); }); it("falls back to the viewport when the strip has no scroll container", () => { const height = stripHeightFor({ stripTop: STRIP_TOP, scrollerBottom: null, viewportHeight: VIEWPORT, }); expect(STRIP_TOP + height).toBeLessThanOrEqual(VIEWPORT); }); it("never returns a negative height when the strip is scrolled past the floor", () => { const height = stripHeightFor({ stripTop: 900, scrollerBottom: 600, viewportHeight: VIEWPORT, }); expect(height).toBe(0); }); });