// @vitest-environment jsdom // // Contract of the shared library search-and-pick list (F6): it owns the debounced query and the // full loading/error/empty/no-results/list state machine so the two callers (AddItemPanel in // PlexCollectionView, NewCollectionPanel in plexBrowseLists) can't drift. The two things a caller // steers — the `enabled` gate (NewCollectionPanel holds it off until the collection name is valid) // and the `emptyHint` shown before a term is typed — are the pieces with no test before this file, so // they're covered here. i18n is key-passthrough and api/debounce are mocked so the test drives state // directly. import { type ComponentProps } from "react"; import { describe, it, expect, afterEach, beforeEach, vi } from "vitest"; import { render, screen, cleanup, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ t: (key: string) => key }) }; }); // Identity debounce so a passed term is reflected immediately. vi.mock("../lib/useDebounced", () => ({ useDebounced: (v: unknown) => v })); // api.plexLibrary stub — returns whatever the current test queued. const plexLibrary = vi.fn(); vi.mock("../lib/api", () => ({ api: { plexLibrary: (...a: unknown[]) => plexLibrary(...a) } })); import PlexLibraryPicker from "./PlexLibraryPicker"; const card = (id: string, title: string, year = 2000) => ({ id, type: "movie" as const, title, year, thumb: `/api/plex/image/${id}`, }); const result = (items: ReturnType[]) => ({ scope: "both", total: items.length, offset: 0, limit: 12, items, episodes: [], }); beforeEach(() => { plexLibrary.mockReset(); plexLibrary.mockResolvedValue(result([])); }); afterEach(cleanup); const renderPicker = (props: Partial> = {}) => { const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); return render( } {...props} /> , ); }; describe("PlexLibraryPicker", () => { it("renders nothing and fires no query while disabled, even with a term", () => { const { container } = renderPicker({ enabled: false, term: "matrix" }); expect(container.firstChild).toBeNull(); expect(plexLibrary).not.toHaveBeenCalled(); }); it("shows the emptyHint (and fires no query) before a term is typed", () => { renderPicker({ term: "", emptyHint:

EMPTY_HINT

}); expect(screen.getByText("EMPTY_HINT")).toBeTruthy(); expect(plexLibrary).not.toHaveBeenCalled(); }); it("queries the library once enabled + a term are both present and renders the rows via renderAction", async () => { plexLibrary.mockResolvedValue(result([card("m1", "The Matrix"), card("m2", "Reloaded")])); renderPicker({ term: "matrix" }); await waitFor(() => expect(screen.getByText("act:m1")).toBeTruthy()); expect(screen.getByText("act:m2")).toBeTruthy(); expect(screen.getByText("The Matrix")).toBeTruthy(); expect(plexLibrary).toHaveBeenCalledWith({ scope: "both", q: "matrix", limit: 12 }); }); it("renders the honest error state (not an empty/no-results state) when the query fails", async () => { plexLibrary.mockRejectedValue(new Error("boom")); renderPicker({ term: "matrix" }); await waitFor(() => expect(screen.getByText("common.loadError")).toBeTruthy()); expect(screen.queryByText("plex.collection.addNoResults")).toBeNull(); }); it("shows the no-results message when the query returns an empty list", async () => { plexLibrary.mockResolvedValue(result([])); renderPicker({ term: "zzzz" }); await waitFor(() => expect(screen.getByText("plex.collection.addNoResults")).toBeTruthy()); }); });