A pure move (git mv, history kept): 333 files leave components/, which now holds only the platform layer (ui/) and the legal pages. The shell is the frame (App, header, nav rail, palette, boot); each page lives in modules/<page>/ with its rail, store and tests; what several pages share sits in features/<name>/; the shared-link page is watch/. Domain-free helpers went straight to ui/ (ViewSwitcher, FilterRail, the confirm dialog, the language switcher, the copy rows). Every relative specifier was rewritten (imports, dynamic imports, vi.mock, new URL), and the contract tests that scan sources now scan the whole app source, not only components/ — each glob was counted before and after so none went green over fewer files. The locale-format rule matched `/src/components/` only, so the move would have taken ~300 files out from under its i18n-singleton check; it now covers every folder that holds components, with a test per folder.
130 lines
5.0 KiB
TypeScript
130 lines
5.0 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// NavigationProvider's URL-derived seeds, backfilling a code review's deferred coverage:
|
|
// • loadInitialPage — the bookmark deep-link precedence (?page > ?v > ?pl > filters/?chan > the
|
|
// account's stored landing page > feed).
|
|
// • channelViewFrom — the shared _chan/?chan resolver behind BOTH the channelView seed and the
|
|
// onPop handler: a popstate landing on a URL-only bookmark entry must restore the
|
|
// channel from ?chan, not close it.
|
|
// Only getAccountRaw is stubbed (the page the account left open); everything else is real. Whether
|
|
// to start on Home is the account's preference, handed in.
|
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
|
|
const { stored } = vi.hoisted(() => ({ stored: {} as Record<string, string | null> }));
|
|
vi.mock("../../lib/storage", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("../../lib/storage")>();
|
|
return { ...actual, getAccountRaw: (k: string) => stored[k] ?? null };
|
|
});
|
|
|
|
import { loadInitialPage, channelViewFrom } from "./navigationInit";
|
|
import { LS } from "../../lib/storage";
|
|
|
|
const setUrl = (search: string) => window.history.replaceState(null, "", search || "/");
|
|
|
|
let startOnHome = true;
|
|
|
|
beforeEach(() => {
|
|
setUrl("/");
|
|
startOnHome = true;
|
|
for (const k of Object.keys(stored)) delete stored[k];
|
|
});
|
|
|
|
describe("loadInitialPage precedence", () => {
|
|
it("?page wins and is validated", () => {
|
|
setUrl("?page=stats");
|
|
expect(loadInitialPage(startOnHome)).toBe("stats");
|
|
});
|
|
it("an invalid ?page falls back to feed", () => {
|
|
setUrl("?page=bogus");
|
|
expect(loadInitialPage(startOnHome)).toBe("feed");
|
|
});
|
|
it("?v implies the plex module", () => {
|
|
setUrl("?v=show:1");
|
|
expect(loadInitialPage(startOnHome)).toBe("plex");
|
|
});
|
|
it("?pl implies playlists", () => {
|
|
setUrl("?pl=3");
|
|
expect(loadInitialPage(startOnHome)).toBe("playlists");
|
|
});
|
|
it.each(["?folder=4", "?item=12", "?folder=all&item=12"])(
|
|
"%s implies the Files page (a folder, or a file to arrive at)",
|
|
(search) => {
|
|
setUrl(search);
|
|
expect(loadInitialPage(startOnHome)).toBe("files");
|
|
},
|
|
);
|
|
it("a filter param implies the feed", () => {
|
|
setUrl("?tags=1,2");
|
|
expect(loadInitialPage(startOnHome)).toBe("feed");
|
|
});
|
|
it("?chan (the channel overlay) implies the feed", () => {
|
|
setUrl("?chan=UC1");
|
|
expect(loadInitialPage(startOnHome)).toBe("feed");
|
|
});
|
|
it("?page beats a lower-precedence param", () => {
|
|
setUrl("?page=stats&v=show:1");
|
|
expect(loadInitialPage(startOnHome)).toBe("stats");
|
|
});
|
|
it("?v beats ?pl and filters", () => {
|
|
setUrl("?v=show:1&pl=3&tags=1");
|
|
expect(loadInitialPage(startOnHome)).toBe("plex");
|
|
});
|
|
// A bare URL lands on Home, OVER the remembered module. Landing on
|
|
// whatever you happened to close the app on is precisely the habit the anti-paralysis surface
|
|
// exists to replace, so the remembered page only wins once the account opts out.
|
|
it("a bare URL lands on Home even when another module is remembered", () => {
|
|
stored[LS.page] = "plex";
|
|
expect(loadInitialPage(startOnHome)).toBe("home");
|
|
});
|
|
it("opting out restores the account's stored landing page", () => {
|
|
startOnHome = false;
|
|
stored[LS.page] = "plex";
|
|
expect(loadInitialPage(startOnHome)).toBe("plex");
|
|
});
|
|
// The Downloads page is gone; what it held is in Files. A bookmark and a remembered page both land
|
|
// there instead of on the feed.
|
|
it("a retired page lands where its content went — linked or remembered", () => {
|
|
setUrl("?page=downloads");
|
|
expect(loadInitialPage(startOnHome)).toBe("files");
|
|
setUrl("/");
|
|
startOnHome = false;
|
|
stored[LS.page] = "downloads";
|
|
expect(loadInitialPage(startOnHome)).toBe("files");
|
|
});
|
|
it("opted out with no / invalid stored page still defaults to feed", () => {
|
|
startOnHome = false;
|
|
expect(loadInitialPage(startOnHome)).toBe("feed");
|
|
stored[LS.page] = "nope";
|
|
expect(loadInitialPage(startOnHome)).toBe("feed");
|
|
});
|
|
it("a deep link still beats Home", () => {
|
|
setUrl("?pl=3");
|
|
expect(loadInitialPage(startOnHome)).toBe("playlists");
|
|
});
|
|
});
|
|
|
|
describe("channelViewFrom", () => {
|
|
it("reads _chan (+ _chanName) from an in-app entry", () => {
|
|
expect(channelViewFrom({ _chan: "UC1", _chanName: "Chan" }, "")).toEqual({
|
|
id: "UC1",
|
|
name: "Chan",
|
|
});
|
|
});
|
|
it("_chan without a name yields an undefined name", () => {
|
|
expect(channelViewFrom({ _chan: "UC1" }, "")).toEqual({ id: "UC1", name: undefined });
|
|
});
|
|
it("falls back to the ?chan URL param when the entry has no _chan (fresh bookmark)", () => {
|
|
expect(channelViewFrom(null, "?chan=UC9")).toEqual({ id: "UC9", name: undefined });
|
|
});
|
|
it("prefers _chan over the URL param", () => {
|
|
expect(channelViewFrom({ _chan: "UCstate" }, "?chan=UCurl")).toEqual({
|
|
id: "UCstate",
|
|
name: undefined,
|
|
});
|
|
});
|
|
it("returns null when neither is present (channel closed)", () => {
|
|
expect(channelViewFrom({}, "")).toBeNull();
|
|
expect(channelViewFrom(null, "/")).toBeNull();
|
|
});
|
|
});
|