// @vitest-environment jsdom import { describe, expect, it } from "vitest"; import { EMPTY_PLEX_FILTERS, type PlexFilters } from "./api"; import { PLEX_SORT_FALLBACK } from "./plexSort"; import { hasPlexParams, plexParamsToState, plexParamUpdates, type PlexUrlState, } from "./plexUrlState"; // A concrete non-default filter set exercising every wire shape (csv arrays, scalars, modes). const RICH_FILTERS: PlexFilters = { genres: ["Drama", "Comedy"], genreMode: "all", contentRatings: ["PG-13"], yearMin: 2000, yearMax: 2020, ratingMin: 7, durationMin: 600, durationMax: 7200, addedWithin: "1m", directors: ["Nolan"], actors: ["Bale", "Caine"], actorMode: "all", studios: ["Warner"], playable: ["direct", "transcode_hw"], keywords: ["heist"], collections: ["1234"], collectionMode: "all", collectionTitles: { "1234": "Marvel" }, }; // Apply an updates delta onto a base URLSearchParams the way urlWithParams would (null deletes). function applyUpdates(updates: Record): URLSearchParams { const p = new URLSearchParams(); for (const [k, v] of Object.entries(updates)) { if (v != null) p.set(k, v); } return p; } const BASE: PlexUrlState = { scope: "both", sort: PLEX_SORT_FALLBACK, show: "all", filters: EMPTY_PLEX_FILTERS, }; describe("plexParamUpdates", () => { it("writes the default view in full: this account remembers one a link must not inherit", () => { expect(plexParamUpdates(BASE)).toEqual({ pscope: "both", psort: "title:asc", pshow: "all", pf: "", }); }); it("writes an unsorted view as such, so a link to it is not sorted by the remembered sort", () => { const unsorted = { ...BASE, sort: null }; expect(plexParamUpdates(unsorted).psort).toBe("none"); const back = plexParamsToState(applyUpdates(plexParamUpdates(unsorted)), { ...BASE, sort: { key: "year", dir: "desc" }, }); expect(back?.sort).toBeNull(); }); it("a link to the default view opens it over another remembered one", () => { const params = applyUpdates(plexParamUpdates(BASE)); const back = plexParamsToState(params, { scope: "movie", sort: { key: "year", dir: "desc" }, show: "watched", filters: RICH_FILTERS, }); expect(back).toEqual(BASE); }); it("clears every key when passed null (off-plex page)", () => { expect(plexParamUpdates(null)).toEqual({ pscope: null, psort: null, pshow: null, pf: null, }); }); it("writes the view picked", () => { const u = plexParamUpdates({ scope: "movie", sort: { key: "year", dir: "desc" }, show: "in_progress", filters: EMPTY_PLEX_FILTERS, }); expect(u.pscope).toBe("movie"); expect(u.psort).toBe("year:desc"); expect(u.pshow).toBe("in_progress"); expect(u.pf).toBe(""); }); }); describe("round-trip", () => { it("restores a rich state from the emitted URL", () => { const state: PlexUrlState = { scope: "show", sort: { key: "rating", dir: "desc" }, show: "watched", filters: RICH_FILTERS, }; const params = applyUpdates(plexParamUpdates(state)); const back = plexParamsToState(params, BASE); expect(back.scope).toBe("show"); expect(back.sort).toEqual({ key: "rating", dir: "desc" }); expect(back.show).toBe("watched"); // collectionTitles is display-only (excluded from the wire), so compare the rest. const { collectionTitles: _drop, ...wire } = RICH_FILTERS; expect(back.filters).toEqual(wire); }); it("keeps base values for absent keys (partial link)", () => { const params = new URLSearchParams("psort=year:asc"); const back = plexParamsToState(params, { scope: "movie", sort: PLEX_SORT_FALLBACK, show: "unwatched", filters: EMPTY_PLEX_FILTERS, }); expect(back.scope).toBe("movie"); // from base, no pscope expect(back.show).toBe("unwatched"); // from base, no pshow expect(back.sort).toEqual({ key: "year", dir: "asc" }); }); it("ignores an out-of-vocabulary scope/show/sort and falls back to base", () => { const params = new URLSearchParams("pscope=bogus&pshow=nope&psort=key:sideways"); const back = plexParamsToState(params, BASE); expect(back.scope).toBe("both"); expect(back.show).toBe("all"); expect(back.sort).toEqual(PLEX_SORT_FALLBACK); // bad dir → parseSortState null → base }); }); describe("hasPlexParams", () => { it("detects any namespaced key", () => { expect(hasPlexParams(new URLSearchParams("pf=genres%3DDrama"))).toBe(true); expect(hasPlexParams(new URLSearchParams("pscope=movie"))).toBe(true); }); it("ignores the feed's bare keys", () => { expect(hasPlexParams(new URLSearchParams("scope=all&sort=oldest&show=watched"))).toBe(false); }); });