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.
102 lines
4.3 KiB
TypeScript
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"]);
|
|
});
|
|
});
|