// @vitest-environment jsdom // // The URL is the view: a page's name search is named in the address, so a reload or a bookmark opens // 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 { 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()), useTranslation: () => ({ t: (k: string) => k, i18n: { language: "en" } }), })); 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, useFilesActions } from "./modules/files/filesContext"; import { usePlaylists, usePlaylistsActions } from "./modules/playlists/playlistsContext"; import { usePlex } from "./modules/plex/plexContext"; import { claimLocalData, getAccountRaw, LS, setAccountRaw } from "./lib/storage"; const seen: { current: Record } = { current: {} }; const nav: { current: NavigationActions | null } = { current: null }; const typeIn: { current: Record void> } = { current: {} }; function Probe() { const channels = useChannels(); const files = useFiles(); const playlists = usePlaylists(); const plex = usePlex(); const actions = useNavigationActions(); const filesActions = useFilesActions(); const playlistsActions = usePlaylistsActions(); useEffect(() => { nav.current = actions; typeIn.current = { files: filesActions.setSearch, playlists: playlistsActions.setSearch }; seen.current = { channels: channels.search, files: files.search, playlists: playlists.search, plex: plex.q, plexField: plex.field, }; }); return null; } beforeEach(() => { localStorage.clear(); claimLocalData(3); }); afterEach(() => { cleanup(); window.history.replaceState(null, "", "/"); }); function mountAt(url: string, page: string) { window.history.replaceState({ sfPage: page }, "", url); render( , ); } const search = () => new URLSearchParams(window.location.search); describe.each([ ["channels", "cq", "tom"], ["files", "fq", "notes"], ["playlists", "plq", "jazz"], ["plex", "pq", "poirot"], ])("%s's name search", (page, key, value) => { it("opens from the address, and stays in it", async () => { mountAt(`/?page=${page}&${key}=${value}`, page); await waitFor(() => expect(seen.current[page]).toBe(value)); expect(search().get(key)).toBe(value); }); }); describe("Plex's search", () => { it("is named in full, an empty one too: this browser remembers one a link must not inherit", async () => { setAccountRaw(LS.plexQ, "poirot"); mountAt("/?page=plex&pq=", "plex"); await waitFor(() => expect(seen.current.plex).toBe("")); expect(search().get("pq")).toBe(""); }); it("takes the field a link names, and remembers the link's search", async () => { mountAt("/?page=plex&pq=hastings&pqf=people", "plex"); await waitFor(() => expect(seen.current.plexField).toBe("people")); expect(seen.current.plex).toBe("hastings"); await waitFor(() => expect(getAccountRaw(LS.plexQ)).toBe("hastings")); 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("")); // At once, not after the typing pause: a reload right after the return opens it unsearched. expect(search().get(key)).toBeNull(); // Typing again within the pause names the new text or nothing, never the ended search. act(() => typeIn.current[page]!("x")); expect(search().get(key)).not.toBe(value); }); });