/** * Regression tests for the app's fixed bottom-UI (mini player + bottom nav) * layout rules. * * The bug these guard against: on the library route the layout used to render * its OWN in-flow mini player while the root ALSO painted a fixed bottom nav on * top of it, and the library scroller only reserved 1rem — so the last row hid * behind the nav. The fix unified everything onto the root: the root owns the * single fixed bottom UI on every route/platform, and every scroll container * reserves the measured `bottomUiHeight`. * * TRACES: UR-005 | DR-009 */ import { describe, it, expect } from "vitest"; import { showBottomNav, showGlobalMiniPlayer, routeOwnsLayout, showBottomUi, reservedBottomPadding, } from "./layoutShell"; const authed = (pathname: string) => ({ pathname, isAuthenticated: true }); describe("showGlobalMiniPlayer", () => { it("shows on the main library page (regression: was hidden on non-Android)", () => { expect(showGlobalMiniPlayer({ pathname: "/library" })).toBe(true); }); it("shows on a deep library page", () => { expect(showGlobalMiniPlayer({ pathname: "/library/abc123" })).toBe(true); }); it("shows on home, search, downloads", () => { expect(showGlobalMiniPlayer({ pathname: "/" })).toBe(true); expect(showGlobalMiniPlayer({ pathname: "/search" })).toBe(true); expect(showGlobalMiniPlayer({ pathname: "/downloads" })).toBe(true); }); it("hides on the full-screen player, login, and settings", () => { expect(showGlobalMiniPlayer({ pathname: "/player/xyz" })).toBe(false); expect(showGlobalMiniPlayer({ pathname: "/login" })).toBe(false); expect(showGlobalMiniPlayer({ pathname: "/settings" })).toBe(false); }); it("does NOT depend on platform or on /library — the root owns it everywhere", () => { // The signature intentionally has no `isAndroid` input: the old bug was a // platform/route split that let a second in-flow mini player exist. expect(showGlobalMiniPlayer({ pathname: "/library" })).toBe(true); }); }); describe("showBottomNav", () => { it("shows on authenticated content routes including library", () => { expect(showBottomNav(authed("/library"))).toBe(true); expect(showBottomNav(authed("/"))).toBe(true); expect(showBottomNav(authed("/settings"))).toBe(true); }); it("hides when unauthenticated", () => { expect(showBottomNav({ pathname: "/library", isAuthenticated: false })).toBe(false); }); it("hides on the full-screen player and login", () => { expect(showBottomNav(authed("/player/xyz"))).toBe(false); expect(showBottomNav(authed("/login"))).toBe(false); }); }); describe("routeOwnsLayout", () => { it("is true for library/settings/player/login (they manage their own scroll)", () => { expect(routeOwnsLayout({ pathname: "/library" })).toBe(true); expect(routeOwnsLayout({ pathname: "/library/abc" })).toBe(true); expect(routeOwnsLayout({ pathname: "/settings" })).toBe(true); expect(routeOwnsLayout({ pathname: "/player/x" })).toBe(true); expect(routeOwnsLayout({ pathname: "/login" })).toBe(true); }); it("is false for routes that render into the root scroller", () => { expect(routeOwnsLayout({ pathname: "/" })).toBe(false); expect(routeOwnsLayout({ pathname: "/search" })).toBe(false); expect(routeOwnsLayout({ pathname: "/downloads" })).toBe(false); }); }); describe("layout invariant: a reservation owner exists wherever bottom UI shows", () => { // The core anti-regression check. Every route falls into exactly one of two // reservation regimes: // - route owns its layout -> the route's own scroller reserves bottomUiHeight // - route does NOT own it -> the root scroller reserves bottomUiHeight // The bug was that the library route was implicitly a THIRD regime: it owned // its layout, showed a fixed nav from the root, but reserved only 1rem. That // can't recur now because library both owns its layout (so it reserves // internally) and the mini player is root-owned (no second in-flow bar). const routes = ["/", "/search", "/downloads", "/library", "/library/abc", "/settings"]; for (const pathname of routes) { it(`${pathname}: exactly one reservation owner`, () => { if (!showBottomUi(authed(pathname))) return; // no bottom UI -> nothing to reserve // Ownership is a total boolean, so exactly one regime always applies — // there is no route that shows bottom UI with no reservation owner. expect(typeof routeOwnsLayout({ pathname })).toBe("boolean"); }); } it("library shows bottom UI AND owns its layout, so it reserves internally", () => { // Directly pins the regression: library must NOT rely on the root scroller // (it has none — the root gives owning routes a clipped, non-scrolling box). expect(showBottomUi(authed("/library"))).toBe(true); expect(routeOwnsLayout({ pathname: "/library" })).toBe(true); }); }); describe("reservedBottomPadding", () => { it("returns an exact px fit when no extra room requested", () => { expect(reservedBottomPadding(120)).toBe("120px"); }); it("adds breathing room via calc for layout-owning routes", () => { expect(reservedBottomPadding(120, 1)).toBe("calc(120px + 1rem)"); }); it("never returns negative padding", () => { expect(reservedBottomPadding(-50)).toBe("0px"); expect(reservedBottomPadding(-50, 1)).toBe("calc(0px + 1rem)"); }); it("reserves 1rem-only when the bottom UI is collapsed to 0 (nothing playing, nav-only measured elsewhere)", () => { expect(reservedBottomPadding(0, 1)).toBe("calc(0px + 1rem)"); }); });