fix(plex): a reload during a binge reopens the episode playing now
The player stepped to the next item in its own state while the history entry still held the item it opened with, so F5 restarted the first episode. The player now says every item it moves on to, and the page rewrites its entry with it (useHistorySubview's `follow`: no navigation, the overlays over the player stay open, and the entry under an overlay is rewritten when a step lands back on it). The player's URL is the item playing (`?v=player:<id>`), so a bookmark opens it; the list it plays from stays in the history entry.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// A reload reopens the item playing NOW (the owner, 2026-10-25): binge a series, press F5 mid-episode,
|
||||
// and the player restarted the episode the session started with — the player moved on in its own
|
||||
// state while the history entry still held the first item. The player says what it moved on to
|
||||
// (PlexPlayer's own tests); this is the page writing it into the entry and the address bar.
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
vi.mock("react-i18next", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("react-i18next")>()),
|
||||
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
||||
}));
|
||||
vi.mock("./PlexPlayer", () => ({
|
||||
default: ({
|
||||
itemId,
|
||||
queue,
|
||||
onItemChange,
|
||||
}: {
|
||||
itemId: string;
|
||||
queue?: string[];
|
||||
onItemChange: (id: string) => void;
|
||||
}) => (
|
||||
<div>
|
||||
<span>
|
||||
player {itemId} queue {queue?.join(",") ?? "none"}
|
||||
</span>
|
||||
<button onClick={() => onItemChange("e2")}>next</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("./plexSavedViews", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("./plexSavedViews")>()),
|
||||
usePlexSavedViews: () => ({ views: [], displayView: null, isDirty: false, activeViewId: null }),
|
||||
}));
|
||||
|
||||
import PlexBrowse from "./PlexBrowse";
|
||||
import { api } from "../../lib/api";
|
||||
|
||||
beforeAll(() => {
|
||||
class Stub {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
takeRecords() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver;
|
||||
globalThis.IntersectionObserver ??= Stub as unknown as typeof IntersectionObserver;
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.spyOn(api, "plexLibrary").mockResolvedValue({
|
||||
items: [],
|
||||
total: 0,
|
||||
offset: 0,
|
||||
episodes: [],
|
||||
} as never);
|
||||
window.history.replaceState(
|
||||
{ _sub: { kind: "player", id: "e1", queue: ["e1", "e2", "e3"] }, sfScroll: 4 },
|
||||
"",
|
||||
"/?page=plex&v=player:e1",
|
||||
);
|
||||
});
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function mount() {
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<PlexBrowse />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("the Plex player on a reload", () => {
|
||||
it("reopens the item it moved on to, with the list it plays from", async () => {
|
||||
mount();
|
||||
expect(await screen.findByText("player e1 queue e1,e2,e3")).toBeTruthy();
|
||||
const before = window.history.length;
|
||||
fireEvent.click(screen.getByText("next"));
|
||||
expect(screen.getByText("player e2 queue e1,e2,e3")).toBeTruthy();
|
||||
expect(window.history.length).toBe(before); // moving on is no step for Back
|
||||
cleanup(); // F5: the page goes, the history entry stays
|
||||
mount();
|
||||
expect(await screen.findByText("player e2 queue e1,e2,e3")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("says the item playing now in the address bar, so a bookmark opens it", async () => {
|
||||
mount();
|
||||
fireEvent.click(await screen.findByText("next"));
|
||||
expect(new URLSearchParams(window.location.search).get("v")).toBe("player:e2");
|
||||
cleanup();
|
||||
window.history.replaceState(null, "", window.location.href); // a fresh tab: the URL alone
|
||||
mount();
|
||||
expect(await screen.findByText("player e2 queue none")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user