- The Feed press over a live search ends only the search: the feed's own view stays named. - Leaving a search steps back only off an entry the app pushed (_ytOwn); one a link or a reload opened is cleared in place, also where the Navigation API is missing. - useUrlMirror rewrites a changed view through replaceView, so a reload's overlay marks go; its first write and its landing writes keep them for an overlay that reopens. - Files and Playlists: a box emptied and filled again settles at once, never naming the ended search. - Playlists: a linked player is scoped to the playlist the store started with, and only the rail's own move to the first playlist is no pick. - Contract: writer objects are scanned to their matching brace, a computed or shorthand key is refused, and every useHistorySubview call hands a named definition. - Lint: playwright/no-skipped-test also refuses fixme (gate snapshot regenerated). - FEED_DEFAULT_SORT at the three remaining sites; linkedNotListed says to search by name.
127 lines
4.8 KiB
TypeScript
127 lines
4.8 KiB
TypeScript
// @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<typeof import("react-i18next")>()),
|
|
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<string, string> } = { current: {} };
|
|
const nav: { current: NavigationActions | null } = { current: null };
|
|
const typeIn: { current: Record<string, (q: string) => 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(
|
|
<QueryClientProvider client={new QueryClient()}>
|
|
<NavigationProvider>
|
|
<ModuleStores>
|
|
<Probe />
|
|
</ModuleStores>
|
|
</NavigationProvider>
|
|
</QueryClientProvider>,
|
|
);
|
|
}
|
|
|
|
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);
|
|
});
|
|
});
|