test(e4): loadInitialPage precedence + channelViewFrom (S1 fix)

Split the two URL-derived nav seeds out of NavigationProvider into
navigationInit.ts so they're unit-testable and the provider stays
react-refresh-clean (component-only exports). channelViewFrom also dedups the
identical _chan/?chan resolve the channelView seed and the onPop handler each
hand-rolled — locking the S1 fix (a popstate on a URL-only bookmark entry must
restore the channel from ?chan, not close it). Pure code motion.

Tests: the full loadInitialPage precedence matrix (?page > ?v > ?pl >
filters/?chan > stored landing > feed, with the beats-a-lower-param cases) and
channelViewFrom (_chan+name, ?chan fallback, state-over-url, closed → null).

15 tests; full FE gate green (tsc/eslint/prettier/knip).
This commit is contained in:
2026-08-20 05:21:13 +02:00
parent 0401dae689
commit 062015ba27
3 changed files with 145 additions and 37 deletions
+39
View File
@@ -0,0 +1,39 @@
// URL-derived navigation seeds, split out of NavigationProvider so they stay unit-testable AND the
// provider file keeps react-refresh happy (component-only exports). `loadInitialPage` picks the landing
// module from a bookmark's params; `channelViewFrom` is the shared _chan/?chan resolver behind BOTH the
// channelView seed and the onPop handler.
import { hasFilterParams, isPage, readPage, type Page } from "../lib/urlState";
import { getAccountRaw, LS } from "../lib/storage";
const PAGE_KEY = LS.page;
export interface ChannelView {
id: string;
name?: string;
}
export function loadInitialPage(): Page {
const params = new URLSearchParams(window.location.search);
if (params.has("page")) return readPage();
// A view-specific param implies its page, so a bookmarked deep link lands on the right module even
// without an explicit ?page. Feed filters (or a channel overlay) imply the feed.
if (params.has("v")) return "plex";
if (params.has("pl")) return "playlists";
if (hasFilterParams(params) || params.has("chan")) return "feed";
// A bare URL → the account's stored landing page.
const stored = getAccountRaw(PAGE_KEY);
return isPage(stored) ? stored : "feed";
}
/** Resolve the open channel page from a landed history entry: the in-app `_chan` id (with its optional
* `_chanName`), else the ?chan URL param — a fresh ?chan bookmark's entry is URL-only. The seed AND
* the popstate handler share this ONE resolver so a popstate that lands on a bookmark entry (no
* `_chan`) restores the channel from the URL instead of wrongly closing it. */
export function channelViewFrom(
state: { _chan?: unknown; _chanName?: unknown } | null | undefined,
search: string,
): ChannelView | null {
const id =
(state?._chan as string | undefined) ?? new URLSearchParams(search).get("chan") ?? undefined;
return id ? { id, name: (state?._chanName as string) ?? undefined } : null;
}