import { describe, expect, it } from "vitest"; import { qk } from "./queryKeys"; import type { FeedFilters } from "./api"; // The factory is the single source of truth for ~200 React Query keys; these tests lock the exact // array shapes it produces, because TanStack invalidation is prefix-MATCHING on those arrays — a // silent shape change (a flipped guard, a dropped spread) would break refetching with no type error. const FILTERS = { scope: "my", tags: [] } as unknown as FeedFilters; describe("qk — bare vs parameterized branch", () => { it("collapses to the bare prefix key only when no arg is given", () => { expect(qk.feed()).toEqual(["feed"]); expect(qk.feed(FILTERS)).toEqual(["feed", FILTERS]); expect(qk.feedSummary()).toEqual(["feed-summary"]); expect(qk.feedSummary(FILTERS)).toEqual(["feed-summary", FILTERS]); expect(qk.playlist()).toEqual(["playlist"]); expect(qk.plexShow()).toEqual(["plex-show"]); expect(qk.plexShow("s1")).toEqual(["plex-show", "s1"]); expect(qk.ytSearch()).toEqual(["yt-search"]); }); it("preserves a NULL segment instead of collapsing it to the prefix", () => { // `["playlist", null]` must stay 2-element — a null id is a real (distinct) key, not "no arg". expect(qk.playlist(null)).toEqual(["playlist", null]); expect(qk.playlist(7)).toEqual(["playlist", 7]); expect(qk.ytSearch(null, 3)).toEqual(["yt-search", null, 3]); expect(qk.ytSearch("cats", 3)).toEqual(["yt-search", "cats", 3]); }); }); describe("qk — single-id and scalar keys", () => { it("returns [root, id]", () => { expect(qk.channel("c1")).toEqual(["channel", "c1"]); expect(qk.videoDetail("v1")).toEqual(["video-detail", "v1"]); expect(qk.playlistMembership("v1")).toEqual(["playlist-membership", "v1"]); expect(qk.plexItem("i1")).toEqual(["plex-item", "i1"]); expect(qk.plexPlaylist(9)).toEqual(["plex-playlist", 9]); expect(qk.downloadLinks(5)).toEqual(["download-links", 5]); expect(qk.dlQuota(3)).toEqual(["dl-quota", 3]); expect(qk.storyboard(5)).toEqual(["storyboard", 5]); expect(qk.adminQuota({ window: "7d" })).toEqual(["admin-quota", { window: "7d" }]); expect(qk.plexPeopleCards("both", ["Actor"])).toEqual(["plex-people-cards", "both", ["Actor"]]); }); it("returns the bare root for no-arg keys", () => { expect(qk.me()).toEqual(["me"]); expect(qk.myStatus()).toEqual(["my-status"]); expect(qk.channels()).toEqual(["channels"]); expect(qk.playlists()).toEqual(["playlists"]); expect(qk.downloads()).toEqual(["downloads"]); expect(qk.adminUsers()).toEqual(["admin-users"]); expect(qk.plex()).toEqual(["plex"]); }); }); describe("qk — variadic composite plex keys", () => { it("spreads the rest args after the root, and is bare with none", () => { expect(qk.plexLibrary()).toEqual(["plex-library"]); expect(qk.plexLibrary("movie", "q", "title", "s", FILTERS)).toEqual([ "plex-library", "movie", "q", "title", "s", FILTERS, ]); expect(qk.plexCollections()).toEqual(["plex-collections"]); expect(qk.plexCollections("lib")).toEqual(["plex-collections", "lib"]); expect(qk.plexCollections("union", "term")).toEqual(["plex-collections", "union", "term"]); expect(qk.plexPlaylists()).toEqual(["plex-playlists"]); expect(qk.plexPlaylists("group", ["1", "2"])).toEqual(["plex-playlists", "group", ["1", "2"]]); expect(qk.plexFacets("both", "s", { genres: [] })).toEqual([ "plex-facets", "both", "s", { genres: [] }, ]); }); it("preserves scope+lang+filters as distinct Discover/Surprise key segments", () => { // Shape-pinning only: these lock that the variadic factory carries every arg through (a "tidy-up" // that drops a param would break cache separation). That the CALL SITE actually passes lang is // guarded separately in PlexDiscover.dom.test.tsx (it inspects the registered query keys). expect(qk.plexDiscover("movie", "hu", FILTERS)).toEqual([ "plex-discover", "movie", "hu", FILTERS, ]); expect(qk.plexSurprise("movie", "hu", FILTERS)).toEqual([ "plex-discover", "surprise", "movie", "hu", FILTERS, ]); // Under the discover root: whatever refreshes the rails refreshes the draw's cards too. expect(qk.plexSurprise()).toEqual(["plex-discover", "surprise"]); }); });