fix(nav): the address follows the view through overlays, reloads and Back (review round 1)

- A URL mirror names the view again on every entry Back or Forward lands on, so a change made
  under a dialog, a drawer or a sub-view is not undone by the next reload.
- A view pushed from the entry a reload under a dialog started from no longer inherits its overlay
  mark; a reload under two overlays closes onto the page in one step.
- The linked player reopens over a live search, gives itself up when its list moves first, and
  plays alone over an emptied playlist; a capped Files list keeps the link it cannot resolve.
- The Feed press over a channel ends the search; leaving a search ends its asked mark; Files and
  Playlists searches end with their page; the feed always names its view and debounces its search.
- Search-box keys are declared per module and kept out of the client log below TRACE.
- Writers outside lib/ go through codecs; the URL contract scans computed keys, overlay writers and
  two-line reads, and holds every page to a bookmark round trip (Config, Activity, Playlists and the
  Files sort and viewer added).
This commit is contained in:
2026-10-27 13:52:42 +01:00
parent 7b4e007680
commit 0d5c6273f1
50 changed files with 928 additions and 157 deletions
+25 -1
View File
@@ -4,7 +4,7 @@
// the list searched (Channels, Files and Playlists kept it in memory only, Plex in this browser only).
// The real navigation and the real stores, read through their hooks; only the network is absent.
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { cleanup, render, waitFor } from "@testing-library/react";
import { act, cleanup, render, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useEffect } from "react";
@@ -14,6 +14,10 @@ vi.mock("react-i18next", async (importOriginal) => ({
}));
import { NavigationProvider } from "./features/navigation/NavigationProvider";
import {
useNavigationActions,
type NavigationActions,
} from "./features/navigation/navigationContext";
import { ModuleStores } from "./features/navigation/ModuleStores";
import { useChannels } from "./modules/channels/channelsContext";
import { useFiles } from "./modules/files/filesContext";
@@ -22,13 +26,16 @@ import { usePlex } from "./modules/plex/plexContext";
import { claimLocalData, getAccountRaw, LS, setAccountRaw } from "./lib/storage";
const seen: { current: Record<string, string> } = { current: {} };
const nav: { current: NavigationActions | null } = { current: null };
function Probe() {
const channels = useChannels();
const files = useFiles();
const playlists = usePlaylists();
const plex = usePlex();
const actions = useNavigationActions();
useEffect(() => {
nav.current = actions;
seen.current = {
channels: channels.search,
files: files.search,
@@ -93,3 +100,20 @@ describe("Plex's search", () => {
expect(search().get("pqf")).toBe("people");
});
});
// The search is the page's while it is on screen: leaving the page ends it, so a trip back through
// the rail opens the page unsearched (and the address names none).
describe.each([
["files", "fq", "notes"],
["playlists", "plq", "jazz"],
])("%s's name search, once the page is left", (page, key, value) => {
it("is gone when the page is opened again", async () => {
mountAt(`/?page=${page}&${key}=${value}`, page);
await waitFor(() => expect(seen.current[page]).toBe(value));
act(() => nav.current!.setPage("feed"));
act(() => nav.current!.setPage(page as "files" | "playlists"));
await waitFor(() => expect(seen.current[page]).toBe(""));
await new Promise((r) => setTimeout(r, 350)); // past the address write's typing pause
expect(search().get(key)).toBeNull();
});
});