fix(plex): Restart only where an episode has a resume point; the Surprise draw is re-read, not redrawn
Show cards carry resumable (ShowState, from the same rollup) so a part-watched show whose next episode is untouched offers Play, not a Continue and a Restart that do the same thing; plexStarted is the one rule for cards and menus. The Surprise rail's key sits under the discover root: whatever refreshes the rails re-reads the draw (keep=) with its watch state now, kept past the 5-minute gc, and only the reshuffle deals. Closing the player refreshes the Discover rails. A show card's play acts on the last click only, and only where it was made.
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
// synchronously. The rails scroll inside a Carousel (useHScroll → ResizeObserver), stubbed for jsdom.
|
||||
import { describe, it, expect, afterEach, beforeAll, vi } from "vitest";
|
||||
import { act, render, screen, fireEvent, cleanup, waitFor } from "@testing-library/react";
|
||||
import { focusManager, QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
beforeAll(() => {
|
||||
class Stub {
|
||||
@@ -57,6 +57,8 @@ vi.mock("../lib/api", () => ({
|
||||
}));
|
||||
|
||||
import PlexDiscover from "./PlexDiscover";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import type { PlexCard } from "../lib/api";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
@@ -157,7 +159,8 @@ describe("PlexDiscover", () => {
|
||||
expect.objectContaining({ genres: [] }),
|
||||
]);
|
||||
expect(keys).toContainEqual([
|
||||
"plex-surprise",
|
||||
"plex-discover",
|
||||
"surprise",
|
||||
"both",
|
||||
"en",
|
||||
expect.objectContaining({ genres: [] }),
|
||||
@@ -219,14 +222,24 @@ describe("PlexDiscover", () => {
|
||||
await waitFor(() => expect(plexSurprise).toHaveBeenCalledTimes(2));
|
||||
expect(plexDiscover).toHaveBeenCalledTimes(1); // the whole surface was NOT refetched
|
||||
// The first draw asks for anything; the redraw hands over the one on screen to deal it last.
|
||||
expect(plexSurprise.mock.calls[0]?.[4]).toEqual([]);
|
||||
expect(plexSurprise.mock.calls[1]?.[4]).toEqual(["sp1"]);
|
||||
expect(plexSurprise.mock.calls[0]?.[4]).toEqual({ exclude: [] });
|
||||
expect(plexSurprise.mock.calls[1]?.[4]).toEqual({ exclude: ["sp1"] });
|
||||
});
|
||||
|
||||
// Back from a card's player or page remounts the surface: the draw it left must still be there.
|
||||
it("keeps its draw across a remount and a return to the window; only ↻ redraws", async () => {
|
||||
// Back from a card's player or page remounts the surface: the draw it left must still be there, with
|
||||
// its cards' watch state as it is now. A refetch reads the same titles again; only ↻ deals.
|
||||
it("keeps its draw across a remount, a refresh and a long absence; only ↻ redraws", async () => {
|
||||
plexDiscover.mockResolvedValue(fullPayload);
|
||||
plexSurprise.mockResolvedValue({ scope: "both", items: [card("sp1", "Random Pick")] });
|
||||
let status: PlexCard["status"] = "new";
|
||||
plexSurprise.mockImplementation(
|
||||
(_s: string, _l: unknown, _f: unknown, _lang: string, o: { keep?: string[] }) =>
|
||||
Promise.resolve({
|
||||
scope: "both",
|
||||
items: o.keep?.length
|
||||
? [{ ...card("sp1", "Random Pick"), status }]
|
||||
: [card(plexSurprise.mock.calls.length > 1 ? "sp2" : "sp1", "Pick")],
|
||||
}),
|
||||
);
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
const ui = (
|
||||
<QueryClientProvider client={qc}>
|
||||
@@ -241,17 +254,24 @@ describe("PlexDiscover", () => {
|
||||
</QueryClientProvider>
|
||||
);
|
||||
const first = render(ui);
|
||||
await screen.findByText("Random Pick");
|
||||
await screen.findByText("Pick");
|
||||
first.unmount();
|
||||
render(ui);
|
||||
await screen.findByText("Random Pick");
|
||||
act(() => {
|
||||
focusManager.setFocused(false);
|
||||
focusManager.setFocused(true);
|
||||
// Watched from somewhere else: whatever refreshes the rails refreshes the draw's cards too.
|
||||
status = "watched";
|
||||
await act(() => qc.invalidateQueries({ queryKey: qk.plexDiscover() }));
|
||||
const [entry] = qc.getQueryCache().findAll({ queryKey: qk.plexSurprise() });
|
||||
expect(entry?.state.data).toMatchObject({ items: [{ id: "sp1", status: "watched" }] });
|
||||
// No draw is ever dealt behind the viewer's back: every call after the first reads it again.
|
||||
for (const call of plexSurprise.mock.calls.slice(1)) expect(call[4]).toEqual({ keep: ["sp1"] });
|
||||
// Unobserved past the default five minutes (a film watched from the rail) it is still kept.
|
||||
expect(entry?.options.gcTime).toBe(Infinity);
|
||||
fireEvent.click(screen.getByRole("button", { name: "plex.discover.reshuffle" }));
|
||||
await screen.findByText("Pick");
|
||||
expect(plexSurprise.mock.calls[plexSurprise.mock.calls.length - 1]?.[4]).toEqual({
|
||||
exclude: ["sp1"],
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
expect(plexSurprise).toHaveBeenCalledTimes(1);
|
||||
focusManager.setFocused(undefined);
|
||||
});
|
||||
|
||||
it("shows the empty message when every rail is empty", async () => {
|
||||
|
||||
Reference in New Issue
Block a user