Files
siftlode/frontend/src/lib/queryKeys.test.ts
T
peter 24448e7c6f feat(activity): one Activity page — runs, admin actions, server log and quota under one filter bar
The audit log, the server log and the admin quota view were three pages (the quota one a Stats tab),
each with its own filters, and the background runs had no list at all. Now one page ("Napló" /
"Activity") reads all four, each still its own table on the server: a shared bar (text, a period of
1 h to 90 days defaulting to 7, an account) and each tab's own filters, the whole view in the URL.
The old page ids open their tab.

Server side: the audit log and the new run list (`/api/admin/runs`) filter and page like the server
log, through one keyset helper (`routes/_keyset`) the log now shares; an audit row keeps the request
it was made in (migration 0111), so its log lines are a pivot away, as a run's are (`lrun`). The
quota breakdown takes since/until and one account. The Scheduler rows get a history button to their
job's runs.

Also fixed on the way: re-saving the value in force on the Configuration page staged its override
without committing it (the commit sat inside the audit branch), so the save was lost.
2026-10-18 03:45:18 +02:00

102 lines
4.3 KiB
TypeScript

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"]);
});
});