feat(player): the YouTube player and the file viewer are named in the address
The player names what it plays in its own history entry (watch, wrel, wtab): a reload or a
bookmark reopens it on the page that hosts it, with that page's list as the queue, or alone
from the catalog (GET /api/videos/{id}/row); a page that hosts none gets the shell's. Back
lands on the page's address without it (useBackToClose's url; ui/Modal passes it on). The
Files viewer and player do the same (fview, fplay). The Plex and channel-manager views are
always written in full, as their fallback is remembered state.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// A reload or a bookmark of the YouTube player reopens it: the page that hosts it opens it once its list
|
||||
// is in — from the list, so it plays on through it, or alone from the catalog — and a page that hosts
|
||||
// none gets the shell's, the video alone. A video the catalog no longer has is said, not swallowed.
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, render, renderHook, screen, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
vi.mock("react-i18next", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("react-i18next")>()),
|
||||
useTranslation: () => ({ t: (k: string) => k, i18n: { language: "en" } }),
|
||||
}));
|
||||
vi.mock("../../lib/actionErrors", () => ({ notifyActionError: vi.fn() }));
|
||||
// The player itself is not what is under test: it shows what it was opened on.
|
||||
vi.mock("./PlayerModal", () => ({
|
||||
default: ({ video, queue }: { video: Video; queue?: Video[] }) => (
|
||||
<p>
|
||||
playing {video.id} {queue ? "in a list" : "alone"}
|
||||
</p>
|
||||
),
|
||||
}));
|
||||
|
||||
import { useOpenLinkedPlayer } from "./useOpenLinkedPlayer";
|
||||
import { LinkedPlayer } from "./LinkedPlayer";
|
||||
import { api, type Video } from "../../lib/api";
|
||||
import { notifyActionError } from "../../lib/actionErrors";
|
||||
|
||||
const vid = (id: string) => ({ id, title: id }) as unknown as Video;
|
||||
|
||||
function wrapper({ children }: { children: ReactNode }) {
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
return <QueryClientProvider client={qc}>{children}</QueryClientProvider>;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
vi.mocked(notifyActionError).mockClear();
|
||||
window.history.replaceState(null, "", "/");
|
||||
});
|
||||
|
||||
describe("useOpenLinkedPlayer", () => {
|
||||
it("opens the item from the host's list once the list is in, and only once", async () => {
|
||||
window.history.replaceState(null, "", "/?watch=B");
|
||||
const row = vi.spyOn(api, "videoRow");
|
||||
const open = vi.fn();
|
||||
const list = [vid("A"), vid("B")];
|
||||
const { rerender } = renderHook(
|
||||
({ ready }) => useOpenLinkedPlayer(ready, (id) => list.find((v) => v.id === id), open),
|
||||
{ wrapper, initialProps: { ready: false } },
|
||||
);
|
||||
expect(open).not.toHaveBeenCalled();
|
||||
rerender({ ready: true });
|
||||
await waitFor(() => expect(open).toHaveBeenCalledWith(list[1], true));
|
||||
rerender({ ready: true });
|
||||
expect(open).toHaveBeenCalledTimes(1);
|
||||
expect(row).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("opens an item the list does not hold alone, its row from the catalog", async () => {
|
||||
window.history.replaceState(null, "", "/?watch=Z");
|
||||
vi.spyOn(api, "videoRow").mockResolvedValue(vid("Z"));
|
||||
const open = vi.fn();
|
||||
renderHook(() => useOpenLinkedPlayer(true, () => undefined, open), { wrapper });
|
||||
await waitFor(() => expect(open).toHaveBeenCalledWith(vid("Z"), false));
|
||||
});
|
||||
|
||||
it("says so when the catalog no longer has it", async () => {
|
||||
window.history.replaceState(null, "", "/?watch=Z");
|
||||
const gone = new Error("not found");
|
||||
vi.spyOn(api, "videoRow").mockRejectedValue(gone);
|
||||
const open = vi.fn();
|
||||
renderHook(() => useOpenLinkedPlayer(true, () => undefined, open), { wrapper });
|
||||
await waitFor(() =>
|
||||
expect(notifyActionError).toHaveBeenCalledWith(gone, "player.linkedFailed"),
|
||||
);
|
||||
expect(open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("opens nothing, and asks for nothing, for an address that names no player", async () => {
|
||||
window.history.replaceState(null, "", "/?q=talks");
|
||||
const row = vi.spyOn(api, "videoRow");
|
||||
const open = vi.fn();
|
||||
renderHook(() => useOpenLinkedPlayer(true, () => undefined, open), { wrapper });
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
expect(open).not.toHaveBeenCalled();
|
||||
expect(row).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("the shell's LinkedPlayer", () => {
|
||||
it("plays the video alone over a page that hosts no player", async () => {
|
||||
window.history.replaceState(null, "", "/?page=channels&watch=Z");
|
||||
vi.spyOn(api, "videoRow").mockResolvedValue(vid("Z"));
|
||||
render(<LinkedPlayer hosted={false} />, { wrapper });
|
||||
expect(await screen.findByText("playing Z alone")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("leaves it to a page that hosts its own", async () => {
|
||||
window.history.replaceState(null, "", "/?watch=Z");
|
||||
const row = vi.spyOn(api, "videoRow").mockResolvedValue(vid("Z"));
|
||||
render(<LinkedPlayer hosted />, { wrapper });
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
expect(row).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText(/playing/)).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user