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:
2026-10-27 05:29:49 +01:00
parent d4f24de4ba
commit fa893c73a3
45 changed files with 1005 additions and 89 deletions
@@ -745,6 +745,51 @@ describe("Files · the viewer", () => {
});
});
// The URL is the view: the file open over the list is named in the address, so a reload or a bookmark
// reopens it once the list is in.
describe("Files · a file the address names", () => {
const doc = (id: number) =>
file(id, {
job_kind: "upload",
display_name: `Doc ${id}`,
asset: { media_kind: "file", preview: "pdf", container: "pdf", size_bytes: 10 },
} as never);
const here = () => new URLSearchParams(window.location.search);
it("reopens the viewer on it, and the viewer names the file it turns to", async () => {
window.history.replaceState(null, "", "/?page=files&fview=2");
mocks.files = [doc(2), doc(3)];
mocks.folders = [YT];
mount();
const dialog = await screen.findByRole("dialog");
expect(dialog.querySelector("iframe")!.getAttribute("src")).toBe(api.previewUrl(2));
await waitFor(() => expect(here().get("fview")).toBe("2"));
fireEvent.keyDown(window, { key: "ArrowRight" });
await waitFor(() => expect(here().get("fview")).toBe("3"));
});
it("reopens the player on a media file", async () => {
window.history.replaceState(null, "", "/?page=files&fplay=1");
mocks.files = [file(1)];
mocks.folders = [YT];
mount();
expect(await screen.findByText("playing 1")).toBeTruthy();
});
it("says so when the list no longer holds it", async () => {
window.history.replaceState(null, "", "/?page=files&fview=99");
mocks.files = [doc(2)];
mocks.folders = [YT];
mount();
await waitFor(() =>
expect(mocks.notify).toHaveBeenCalledWith(
expect.objectContaining({ level: "warning", message: "files.linkedGone" }),
),
);
expect(screen.queryByRole("dialog")).toBeNull();
});
});
describe("Files · the view", () => {
it("starts the list at the scroller's top, so its sticky header does not jump on the first scroll", () => {
mocks.files = [file(2)];