feat(plex): every show card plays the episode it would put on
A show card's picture opened the show page everywhere but Home, whose shelves carried the
resolved `next`. Now every Plex picture plays: a show asks `GET /api/plex/show/{id}/next`
(the in-progress episode, else the first unwatched, else S1·E1 — the rule Home and the show
page's Resume follow) on the click, fresh each time. A started show shows Continue + Restart;
its title, an ⓘ and the menu's "Open show" row open its page; one with no episodes opens there.
The question is asked per show, never carried on every card: three probes a show would cost a
grid page up to 180 queries. The hover preview asks it once it opens and names the episode
("Next: S6·E16 — Title"). Related shows on a show page play the same way.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
// @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<typeof import("react-i18next")>()),
|
||||
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 } = renderHook(() => usePlayShow(play, openShow), {
|
||||
wrapper: ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={qc}>{children}</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
return { qc, play, openShow, playShow: result.current };
|
||||
}
|
||||
|
||||
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("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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user