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.
112 lines
4.5 KiB
TypeScript
112 lines
4.5 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// The channel page opens over any page, and its address names the channel alone (the owner, 2026-10-05:
|
|
// it used to carry the feed's filters, naming a sort the channel did not show). The page under it is
|
|
// still mounted — its store lives at the root — and a change in it while the channel stands over it
|
|
// (a background refetch, the header search) must not write its keys onto the channel's address. Closing
|
|
// the channel brings them back. The real provider and the real stores; only the network is absent.
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { act, cleanup, render, waitFor } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { useEffect } from "react";
|
|
|
|
vi.mock("react-i18next", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("react-i18next")>()),
|
|
useTranslation: () => ({ t: (k: string) => k, i18n: { language: "en" } }),
|
|
}));
|
|
|
|
import { NavigationProvider } from "./NavigationProvider";
|
|
import { ModuleStores } from "./ModuleStores";
|
|
import { useNavigationActions, type NavigationActions } from "./navigationContext";
|
|
import { useFeedFilters, useFeedFiltersActions } from "../../modules/feed";
|
|
import { usePlexActions } from "../../modules/plex";
|
|
import { useChannelsActions } from "../../modules/channels";
|
|
import { useFilesActions } from "../../modules/files";
|
|
import { usePlaylistsActions } from "../../modules/playlists";
|
|
import { claimLocalData } from "../../lib/storage";
|
|
|
|
type Probe = {
|
|
nav: NavigationActions;
|
|
change: Record<string, () => void>;
|
|
};
|
|
const probe: { current: Probe | null } = { current: null };
|
|
|
|
function Probe() {
|
|
const nav = useNavigationActions();
|
|
const filters = useFeedFilters();
|
|
const { setFilters } = useFeedFiltersActions();
|
|
const plex = usePlexActions();
|
|
const channels = useChannelsActions();
|
|
const files = useFilesActions();
|
|
const playlists = usePlaylistsActions();
|
|
// Handed out after each render (an effect, not the render itself, may write outside it).
|
|
useEffect(() => {
|
|
probe.current = {
|
|
nav,
|
|
change: {
|
|
feed: () => setFilters({ ...filters, sort: "oldest" }),
|
|
plex: () => plex.setScope("movie"),
|
|
channels: () => channels.setView("hidden"),
|
|
files: () => files.setView({ kind: "folder", id: 3 }),
|
|
playlists: () => playlists.setSelectedId(5),
|
|
"plex search": () => plex.setQ("poirot"),
|
|
"channels search": () => channels.setSearch("tom"),
|
|
"files search": () => files.setSearch("notes"),
|
|
"playlists search": () => playlists.setSearch("jazz"),
|
|
},
|
|
};
|
|
});
|
|
return null;
|
|
}
|
|
|
|
// What each change above writes, once its page is what the address shows: [page, key, value]. A
|
|
// search is written once the typing pauses.
|
|
const WRITTEN: Record<string, [string, string, string]> = {
|
|
feed: ["feed", "sort", "oldest"],
|
|
plex: ["plex", "pscope", "movie"],
|
|
channels: ["channels", "ctab", "hidden"],
|
|
files: ["files", "folder", "3"],
|
|
playlists: ["playlists", "pl", "5"],
|
|
"plex search": ["plex", "pq", "poirot"],
|
|
"channels search": ["channels", "cq", "tom"],
|
|
"files search": ["files", "fq", "notes"],
|
|
"playlists search": ["playlists", "plq", "jazz"],
|
|
};
|
|
const PAUSE = () => new Promise((r) => setTimeout(r, 400));
|
|
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
claimLocalData(3);
|
|
});
|
|
afterEach(() => {
|
|
cleanup();
|
|
window.history.replaceState(null, "", "/");
|
|
});
|
|
|
|
const search = () => new URLSearchParams(window.location.search);
|
|
|
|
describe.each(Object.keys(WRITTEN))("a channel page over %s", (change) => {
|
|
it("keeps the page's keys off its address, and they come back when it closes", async () => {
|
|
const [page, key, value] = WRITTEN[change]!;
|
|
// The page named even for the feed: a bare address opens on Home (the account's default).
|
|
window.history.replaceState({ sfPage: page }, "", `/?page=${page}`);
|
|
render(
|
|
<QueryClientProvider client={new QueryClient()}>
|
|
<NavigationProvider>
|
|
<ModuleStores>
|
|
<Probe />
|
|
</ModuleStores>
|
|
</NavigationProvider>
|
|
</QueryClientProvider>,
|
|
);
|
|
act(() => probe.current!.nav.openChannel("UC1", "One"));
|
|
act(() => probe.current!.change[change]!());
|
|
await act(PAUSE);
|
|
expect(search().get(key)).toBeNull();
|
|
expect(search().get("chan")).toBe("UC1");
|
|
act(() => probe.current!.nav.closeChannel());
|
|
await waitFor(() => expect(search().get("chan")).toBeNull());
|
|
await waitFor(() => expect(search().get(key)).toBe(value));
|
|
});
|
|
});
|