// @vitest-environment jsdom // // A show's card plays (the owner, 2026-10-09): the click asks the server which episode, every time — // a preview answer from before a watch elsewhere must not decide it. import { describe, it, expect, vi, afterEach } from "vitest"; import { renderHook, act } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { ReactNode } from "react"; const notified = vi.hoisted(() => vi.fn()); vi.mock("../lib/actionErrors", () => ({ notifyActionError: notified })); vi.mock("react-i18next", async (importOriginal) => ({ ...(await importOriginal()), useTranslation: () => ({ t: (k: string) => k, i18n: { language: "hu" } }), })); import { api, type PlexCard } from "../lib/api"; import { qk } from "../lib/queryKeys"; import { usePlayShow } from "./usePlayShow"; afterEach(() => { vi.restoreAllMocks(); notified.mockReset(); }); const show: PlexCard = { id: "sh", type: "show", title: "Show", thumb: "" }; const ep = (id: string): PlexCard => ({ id, type: "episode", title: id, thumb: "" }); function mount() { const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const play = vi.fn(); const openShow = vi.fn(); const { result, rerender, unmount } = renderHook( ({ here }: { here: unknown }) => usePlayShow(play, openShow, here), { initialProps: { here: "grid" as unknown }, wrapper: ({ children }: { children: ReactNode }) => ( {children} ), }, ); return { qc, play, openShow, playShow: result.current, rerender, unmount }; } /** Answers the test hands out one by one, in the order it chooses. */ function answers() { const pending: ((v: { next: PlexCard | null }) => void)[] = []; vi.spyOn(api, "plexShowNext").mockImplementation(() => new Promise((r) => void pending.push(r))); return pending; } describe("usePlayShow", () => { it("plays the episode the server names, from the start when asked to", async () => { const ask = vi.spyOn(api, "plexShowNext").mockResolvedValue({ next: ep("e7") }); const { play, openShow, playShow } = mount(); await act(() => playShow(show, true)); expect(ask).toHaveBeenCalledWith("sh", "hu"); expect(play).toHaveBeenCalledWith("e7", true); expect(openShow).not.toHaveBeenCalled(); }); it("asks again rather than play what the preview cached before a watch", async () => { const ask = vi.spyOn(api, "plexShowNext").mockResolvedValue({ next: ep("e8") }); const { qc, play, playShow } = mount(); qc.setQueryData(qk.plexShowNext("sh", "hu"), { next: ep("e7") }); await act(() => playShow(show, false)); expect(ask).toHaveBeenCalledTimes(1); expect(play).toHaveBeenCalledWith("e8", false); }); it("opens the page of a show with nothing to play", async () => { vi.spyOn(api, "plexShowNext").mockResolvedValue({ next: null }); const { play, openShow, playShow } = mount(); await act(() => playShow(show, false)); expect(openShow).toHaveBeenCalledWith("sh"); expect(play).not.toHaveBeenCalled(); }); it("acts on the last click only: an earlier, slower answer is dropped", async () => { const pending = answers(); const { play, playShow } = mount(); const first = playShow({ ...show, id: "a" }, false); const second = playShow({ ...show, id: "b" }, false); await act(async () => pending[1]!({ next: ep("b1") })); await act(async () => pending[0]!({ next: ep("a1") })); await act(() => Promise.all([first, second])); expect(play.mock.calls).toEqual([["b1", false]]); }); it("drops an answer that lands after the viewer has moved on, or left", async () => { const pending = answers(); const { play, openShow, playShow, rerender, unmount } = mount(); const moved = playShow(show, false); rerender({ here: "info" }); await act(async () => pending[0]!({ next: ep("e1") })); await act(() => moved); expect(play).not.toHaveBeenCalled(); const left = playShow(show, false); unmount(); await act(async () => pending[1]!({ next: null })); await act(() => left); expect(openShow).not.toHaveBeenCalled(); }); it("says so when the question fails, and opens nothing", async () => { vi.spyOn(api, "plexShowNext").mockRejectedValue(new Error("down")); const { play, openShow, playShow } = mount(); await act(() => playShow(show, false)); expect(notified).toHaveBeenCalledWith(expect.any(Error), "plex.playShowFailed"); expect(play).not.toHaveBeenCalled(); expect(openShow).not.toHaveBeenCalled(); }); });