fix(plex): resolve deep-review findings on the filter-bar overhaul
Self-review of the branch diff (feature/plex-filter-bar-overhaul) against main: - Surprise rail query key omitted `lang` while the backend /surprise now filters against the language-specific genre column → a HU/EN switch under an active genre filter left the rail stale. Key it by scope+lang+filters, matching plexDiscover. - Chip-removing a collection left its entry in `collectionTitles` (hashed into the plex query keys), splitting the cache from the pristine no-collection state and growing the titles map unboundedly. Prune the title on remove, like CollectionPicker. - The saved-views default-mirror effect gained `cfg` in its deps (re-memoized on every live filter edit) → a synchronous localStorage write per keystroke. Depend on the module-stable mirror fn instead. - Collections-browse search input had no accessible name (placeholder only). Add aria-label + type="search". - Extract the tile hue-hash + gradient duplicated between PlexDiscover and plexBrowseLists into a shared lib/hueTile so the two surfaces can't drift. - Document migration 0073 downgrade() as lossy and de-dup non-feed rows before recreating the narrow (user_id, name) constraint (verified by the drift test). - Add unit tests for coercePlexFilters (legacy single-collection migration) and plexFiltersSignature (order-independence, title-ignoring). Gate: tsc/lint/knip/prettier clean, 410 vitest; backend ruff + migration drift + saved-views tests green.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
// @vitest-environment jsdom
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
coercePlexFilters,
|
||||
plexFiltersSignature,
|
||||
EMPTY_PLEX_FILTERS,
|
||||
type PlexFilters,
|
||||
} from "./plex";
|
||||
|
||||
describe("coercePlexFilters", () => {
|
||||
it("migrates the legacy single collection/collectionTitle into the multi-select shape", () => {
|
||||
const out = coercePlexFilters({ collection: "A", collectionTitle: "Marvel" });
|
||||
expect(out.collections).toEqual(["A"]);
|
||||
expect(out.collectionTitles).toEqual({ A: "Marvel" });
|
||||
});
|
||||
|
||||
it("migrates a legacy collection with no title", () => {
|
||||
const out = coercePlexFilters({ collection: "A" });
|
||||
expect(out.collections).toEqual(["A"]);
|
||||
expect(out.collectionTitles).toBeUndefined();
|
||||
});
|
||||
|
||||
it("leaves an already-multi collections blob untouched (guard on !collections.length)", () => {
|
||||
const out = coercePlexFilters({ collections: ["X", "Y"], collection: "A" });
|
||||
expect(out.collections).toEqual(["X", "Y"]);
|
||||
});
|
||||
|
||||
it("fills every default for an empty blob", () => {
|
||||
expect(coercePlexFilters({})).toEqual(EMPTY_PLEX_FILTERS);
|
||||
expect(coercePlexFilters({}).collections).toEqual([]);
|
||||
});
|
||||
|
||||
it("drops the legacy sortDir field", () => {
|
||||
const out = coercePlexFilters({ sortDir: "desc", genres: ["Drama"] });
|
||||
expect((out as unknown as Record<string, unknown>).sortDir).toBeUndefined();
|
||||
expect(out.genres).toEqual(["Drama"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plexFiltersSignature", () => {
|
||||
const base = (): PlexFilters => ({
|
||||
...EMPTY_PLEX_FILTERS,
|
||||
genres: ["Drama"],
|
||||
collections: ["A"],
|
||||
});
|
||||
|
||||
it("is independent of key order for equivalent filters", () => {
|
||||
// Same field values, keys assigned in a different literal order — appendPlexFilters emits by field,
|
||||
// not by object key order, so the signatures must match.
|
||||
const a: PlexFilters = { ...EMPTY_PLEX_FILTERS, genres: ["Drama"], collections: ["A"] };
|
||||
const b: PlexFilters = { ...EMPTY_PLEX_FILTERS, collections: ["A"], genres: ["Drama"] };
|
||||
expect(plexFiltersSignature(a)).toBe(plexFiltersSignature(b));
|
||||
});
|
||||
|
||||
it("ignores display-only collectionTitles (not part of the wire params)", () => {
|
||||
const a = base();
|
||||
const b: PlexFilters = { ...base(), collectionTitles: { A: "Marvel" } };
|
||||
expect(plexFiltersSignature(a)).toBe(plexFiltersSignature(b));
|
||||
});
|
||||
|
||||
it("differs when a real filter differs", () => {
|
||||
const a = base();
|
||||
const b: PlexFilters = { ...base(), genres: ["Comedy"] };
|
||||
expect(plexFiltersSignature(a)).not.toBe(plexFiltersSignature(b));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user