fix(plex): round-2 deep-review follow-ups on the review fixes

Second review round, scoped to the round-1 fix commit (where round-1's own fixes
introduced new code):

- The collection chip-remove pruned collectionTitles to an empty {} on last removal,
  which still hashes differently from the pristine no-collection state (key absent) in
  the React Query keys → a residual cache split. Collapse an empty map to undefined so
  the key-shape matches the pristine one.
- Replace the tautological "order-independent signature" test (both objects spread from
  EMPTY_PLEX_FILTERS inherited the same key order, so it proved nothing) with full
  literals in genuinely different insertion order that would fail an order-sensitive
  JSON.stringify signature.
- Cover the collectionTitles chip-remove prune (usePlexActiveFilters.dom.test.tsx) and
  the plexSurprise/plexDiscover lang-key split (queryKeys.test.ts) with regression tests.

Gate: tsc/lint/knip/prettier clean, 412 vitest.
This commit is contained in:
2026-08-23 01:53:33 +02:00
parent b7a4eaff8d
commit 8f1309fa18
4 changed files with 61 additions and 7 deletions
@@ -18,7 +18,12 @@ const plexState = {
show: "watched",
// A non-default sort is present in the shared state — the hook must IGNORE it (no sort chip).
sort: { key: "year", dir: "asc" },
filters: { ...EMPTY_PLEX_FILTERS, genres: ["Action"] },
filters: {
...EMPTY_PLEX_FILTERS,
genres: ["Action"],
collections: ["A"],
collectionTitles: { A: "Marvel" },
},
};
const setShow = vi.fn();
const setFilters = vi.fn();
@@ -40,4 +45,21 @@ describe("usePlexActiveFilters", () => {
expect(keys).not.toContain("sort");
expect(result.current.every((c) => !c.key.startsWith("sort"))).toBe(true);
});
it("removing the last collection chip prunes its title and collapses collectionTitles to undefined", () => {
setFilters.mockClear();
const { result } = renderHook(() => usePlexActiveFilters());
const chip = result.current.find((c) => c.key === "collection:A");
expect(chip).toBeDefined();
chip!.remove();
// patch() merges into the full filters object; assert the two touched fields directly. No residual
// { A: "Marvel" } and no empty {} (which would still split the query-key cache from the pristine
// no-collection state) — collectionTitles must be undefined.
const arg = setFilters.mock.calls[0]![0] as {
collections: string[];
collectionTitles?: unknown;
};
expect(arg.collections).toEqual([]);
expect(arg.collectionTitles).toBeUndefined();
});
});
@@ -123,11 +123,13 @@ export function buildPlexActiveFilters(
label: filters.collectionTitles?.[c] ?? t("plex.filter.collection"),
// Prune the title too — collectionTitles is hashed into the plex query keys, so a stale entry
// splits the cache from the logically-identical no-collection state (mirrors CollectionPicker).
// Collapse an empty map back to undefined so the last-removed state key-matches the pristine
// one (an empty `{}` still hashes differently from an absent key).
remove: () => {
const { [c]: _drop, ...restTitles } = filters.collectionTitles ?? {};
patch({
collections: filters.collections.filter((x) => x !== c),
collectionTitles: restTitles,
collectionTitles: Object.keys(restTitles).length ? restTitles : undefined,
});
},
});