E32 review round 1 (F3–F6). - Channels' open-row store and the chat dock use lib/store's createStore instead of a hand-rolled listener set (the chat dock gets a test of its own). - The Plex backdrop reads prefers-reduced-motion through useMediaQuery; the query string is one constant (REDUCED_MOTION) for the hook and the carousel's one-off read. - A link-restored collection filter showed its raw rating_key in the picker while the titles loaded: usePlexCollectionTitles now says it is pending, the picker draws a placeholder (labelsPending, as the channel picker does) and so does the chip row. - The "deleted tag" rule was written twice with two i18n keys and had drifted: the channel manager showed NO chip for a pick while the tag list loaded, or ever after a failed load, while the pick still narrowed the table. One rule now (lib/tagPicks), one key (common.deletedTag); the manager's row shows every pick, as a placeholder while loading and as its id after a failure.
112 lines
4.9 KiB
TypeScript
112 lines
4.9 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// A shared Plex link carries the collection filter as bare rating_keys — `?pf=collections%3D17045`
|
|
// says nothing about what 17045 is called. Opened cold, the sidebar token rendered that raw id and
|
|
// the applied-filter chip could only say "Collection". This hook resolves the missing ones.
|
|
//
|
|
// Two things it must NOT do: ask the server for a title the filter state already carries (the common
|
|
// case — the user picked the collection in the sidebar), and pull the whole collection list to find
|
|
// one name (431 collections / ~200 KB on the owner's library).
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { renderHook, waitFor } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { createElement, type ReactNode } from "react";
|
|
|
|
const plexCollections = vi.fn();
|
|
vi.mock("./api", () => ({ api: { plexCollections: (...a: unknown[]) => plexCollections(...a) } }));
|
|
|
|
import { usePlexCollectionTitles } from "./usePlexCollectionTitles";
|
|
|
|
const wrap = () => {
|
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
return ({ children }: { children: ReactNode }) =>
|
|
createElement(QueryClientProvider, { client: qc }, children);
|
|
};
|
|
|
|
const CATALOG: Record<string, string> = {
|
|
"17045": "Pirates of the Caribbean Collection",
|
|
"68231": "IMDb Top 250",
|
|
};
|
|
|
|
beforeEach(() => {
|
|
plexCollections.mockReset();
|
|
// Answer like the endpoint does: only the requested keys come back.
|
|
plexCollections.mockImplementation((_lib: unknown, _q: unknown, keys: string[]) =>
|
|
Promise.resolve({
|
|
collections: (keys ?? []).map((id) => ({ id, title: CATALOG[id] })),
|
|
}),
|
|
);
|
|
});
|
|
|
|
describe("usePlexCollectionTitles", () => {
|
|
it("resolves the ids the filter state has no title for", async () => {
|
|
const { result } = renderHook(() => usePlexCollectionTitles(["17045"], undefined), {
|
|
wrapper: wrap(),
|
|
});
|
|
await waitFor(() =>
|
|
expect(result.current.titles["17045"]).toBe("Pirates of the Caribbean Collection"),
|
|
);
|
|
// Only the missing ids are asked for — nothing else.
|
|
expect(plexCollections).toHaveBeenCalledWith(undefined, undefined, ["17045"]);
|
|
});
|
|
|
|
it("asks for nothing when every id already has a title", async () => {
|
|
const known = { "17045": "Pirates of the Caribbean Collection" };
|
|
const { result } = renderHook(() => usePlexCollectionTitles(["17045"], known), {
|
|
wrapper: wrap(),
|
|
});
|
|
expect(result.current.titles["17045"]).toBe("Pirates of the Caribbean Collection");
|
|
// Give a would-be fetch a chance to happen before asserting it did not.
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
expect(plexCollections).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("asks only for the missing half of a mixed selection", async () => {
|
|
const { result } = renderHook(
|
|
() => usePlexCollectionTitles(["17045", "68231"], { "17045": "Already Known" }),
|
|
{ wrapper: wrap() },
|
|
);
|
|
await waitFor(() => expect(result.current.titles["68231"]).toBe("IMDb Top 250"));
|
|
// The known id is not in the request — and its stored title survives the merge.
|
|
expect(plexCollections).toHaveBeenCalledWith(undefined, undefined, ["68231"]);
|
|
expect(result.current.titles["17045"]).toBe("Already Known");
|
|
});
|
|
|
|
it("treats the same ids in a different order as one query", async () => {
|
|
const w = wrap();
|
|
const a = renderHook(() => usePlexCollectionTitles(["68231", "17045"], undefined), {
|
|
wrapper: w,
|
|
});
|
|
await waitFor(() => expect(a.result.current.titles["17045"]).toBeDefined());
|
|
const calls = plexCollections.mock.calls.length;
|
|
const b = renderHook(() => usePlexCollectionTitles(["17045", "68231"], undefined), {
|
|
wrapper: w,
|
|
});
|
|
await waitFor(() => expect(b.result.current.titles["17045"]).toBeDefined());
|
|
expect(plexCollections.mock.calls.length).toBe(calls);
|
|
});
|
|
|
|
it("returns nothing extra while the lookup is still in flight", () => {
|
|
plexCollections.mockReturnValue(new Promise(() => {}));
|
|
const { result } = renderHook(() => usePlexCollectionTitles(["17045"], undefined), {
|
|
wrapper: wrap(),
|
|
});
|
|
// The caller falls back to its own label; it must not be handed a half-built map — and it is
|
|
// told the names are on their way, so it draws a placeholder rather than the raw id.
|
|
expect(result.current.titles["17045"]).toBeUndefined();
|
|
expect(result.current.pending).toBe(true);
|
|
});
|
|
|
|
it("is not pending when nothing is missing, nor once the lookup answered", async () => {
|
|
const known = renderHook(() => usePlexCollectionTitles(["17045"], { "17045": "Known" }), {
|
|
wrapper: wrap(),
|
|
});
|
|
expect(known.result.current.pending).toBe(false);
|
|
const cold = renderHook(() => usePlexCollectionTitles(["17045"], undefined), {
|
|
wrapper: wrap(),
|
|
});
|
|
await waitFor(() => expect(cold.result.current.pending).toBe(false));
|
|
expect(cold.result.current.titles["17045"]).toBe("Pirates of the Caribbean Collection");
|
|
});
|
|
});
|