feat(nav): tabs, list views and name searches are named in the address

Settings, Configuration and Users name their tab (stab/cfgtab/utab); a link's tab wins and a tab pre-selected
while its page is on screen now switches it. A playlist's sort, filters and grouping (plsort/plf/plgroup), the
Files sort (fsort, always written) and the name searches of Channels, Files, Playlists and Plex (cq/fq/plq/pq+pqf)
reopen from the address; the searches are written once the typing pauses.
This commit is contained in:
2026-10-26 01:13:39 +01:00
parent 91d36b3357
commit d4f24de4ba
34 changed files with 728 additions and 130 deletions
@@ -0,0 +1,70 @@
// @vitest-environment jsdom
//
// The URL is the view: Settings' tab is named in the address, so a reload or a bookmark opens the tab
// it showed (it lived in sessionStorage alone: a bookmark opened Appearance). And a notice's link to a
// tab, followed while Settings is already on screen, turns to it — the navigation to the page it is on
// is a no-op, so the pre-selection used to wait for the next visit.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
vi.mock("react-i18next", async (importOriginal) => ({
...(await importOriginal<typeof import("react-i18next")>()),
useTranslation: () => ({ t: (k: string) => k, i18n: { language: "en" } }),
}));
vi.mock("../../lib/useMe", () => ({ useMe: () => ({ id: 1, email: "a@x", role: "user" }) }));
vi.mock("../../features/prefs/prefsContext", () => ({ usePrefs: () => ({}) }));
vi.mock("../../features/wizard/wizardContext", () => ({
useWizard: () => ({ openWizard: () => {} }),
}));
// The tabs' own content is not what is under test.
vi.mock("./ApiTokens", () => ({ default: () => <p>tokens-tab</p> }));
vi.mock("./DiagnosticsSettings", () => ({ default: () => <p>diagnostics-tab</p> }));
import SettingsPanel from "./SettingsPanel";
import { focusSettingsTab } from "../../lib/settingsTab";
import { claimLocalData } from "../../lib/storage";
beforeEach(() => {
claimLocalData(1);
});
afterEach(() => {
cleanup();
sessionStorage.clear();
localStorage.clear();
window.history.replaceState(null, "", "/");
});
const search = () => new URLSearchParams(window.location.search);
const mount = () =>
render(
<QueryClientProvider client={new QueryClient()}>
<SettingsPanel />
</QueryClientProvider>,
);
describe("Settings' tab in the address", () => {
it("opens on the tab a link names, over the one this browser tab last showed", () => {
focusSettingsTab("diagnostics");
window.history.replaceState(null, "", "/?page=settings&stab=tokens");
mount();
expect(screen.getByText("tokens-tab")).toBeTruthy();
expect(search().get("stab")).toBe("tokens");
});
it("names the tab picked", () => {
window.history.replaceState(null, "", "/?page=settings&stab=tokens");
mount();
fireEvent.click(screen.getByTestId("settings-tab-diagnostics"));
expect(screen.getByText("diagnostics-tab")).toBeTruthy();
expect(search().get("stab")).toBe("diagnostics");
});
it("turns to a tab pre-selected while the page is on screen", () => {
window.history.replaceState(null, "", "/?page=settings&stab=tokens");
mount();
act(() => focusSettingsTab("diagnostics"));
expect(screen.getByText("diagnostics-tab")).toBeTruthy();
expect(search().get("stab")).toBe("diagnostics");
});
});