A session's marks are weighed per field against when each page of a never-refetched list was read: a page read before the mark (a Show more / Load more on its way) takes it, one read after keeps the server's word. The live search lays them over its rows too, not only the related list. Items of the built-in Watch later list ARE the bookmark, so adding, removing and moving them go through features/video/videoWrites and mark the video; the contract test covers those writes and catches a write that is named without being called on the spot. The player's watched toggle follows a mark confirmed elsewhere since it opened (the related row's menu, a notice's Undo); Restart on the related row after a description link goes back to it. The file viewer tells the help sheet its keys wait under a layer; refusalReason replaces the last two open-coded reads. Auto-advance wears list glyphs of its own (the skip arrows are Previous / Next beside it), and the auto-advance and loop cycles are `lit`, not toggles: no aria-pressed, the name says the mode. Both places render from one list of controls, so the docked tooltips name the mode too. The English cap message and admin hint quote "Show more"; the hint says refunds are limited.
96 lines
3.6 KiB
TypeScript
96 lines
3.6 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// Each write reaches the lists that are never refetched once the server has it — and not before:
|
|
// a refused write leaves them as they were.
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { QueryClient } from "@tanstack/react-query";
|
|
|
|
import { api } from "../../lib/api";
|
|
import { qk } from "../../lib/queryKeys";
|
|
import {
|
|
addPlaylistItem,
|
|
movePlaylistItem,
|
|
removePlaylistItem,
|
|
resetVideoState,
|
|
writeVideoState,
|
|
writeWatchLater,
|
|
} from "./videoWrites";
|
|
|
|
afterEach(() => vi.restoreAllMocks());
|
|
|
|
// The marks' values, without when each was confirmed.
|
|
const marks = (qc: QueryClient) => {
|
|
const m = qc.getQueryData<Record<string, Record<string, { value: unknown }>>>(qk.videoMarks());
|
|
return m
|
|
? Object.fromEntries(
|
|
Object.entries(m).map(([id, f]) => [
|
|
id,
|
|
Object.fromEntries(Object.entries(f).map(([k, x]) => [k, x.value])),
|
|
]),
|
|
)
|
|
: undefined;
|
|
};
|
|
const WATCH_LATER = { id: 1, kind: "watch_later" as const };
|
|
const MINE = { id: 2, kind: "user" as const };
|
|
|
|
describe("videoWrites", () => {
|
|
it("marks a state written", async () => {
|
|
vi.spyOn(api, "setState").mockResolvedValue({ ok: true } as never);
|
|
const qc = new QueryClient();
|
|
await writeVideoState(qc, "X", "hidden");
|
|
expect(marks(qc)).toEqual({ X: { status: "hidden" } });
|
|
});
|
|
|
|
it("marks a reset as new, from the start", async () => {
|
|
vi.spyOn(api, "clearState").mockResolvedValue({ ok: true } as never);
|
|
const qc = new QueryClient();
|
|
await resetVideoState(qc, "X");
|
|
expect(marks(qc)).toEqual({ X: { status: "new", position_seconds: 0 } });
|
|
});
|
|
|
|
it("marks a Watch-later save and its removal", async () => {
|
|
vi.spyOn(api, "watchLaterAdd").mockResolvedValue({ ok: true } as never);
|
|
vi.spyOn(api, "watchLaterRemove").mockResolvedValue({ ok: true } as never);
|
|
const qc = new QueryClient();
|
|
await writeWatchLater(qc, "X", true);
|
|
expect(marks(qc)).toEqual({ X: { saved: true } });
|
|
await writeWatchLater(qc, "X", false);
|
|
expect(marks(qc)).toEqual({ X: { saved: false } });
|
|
});
|
|
|
|
it("marks an item of the Watch later list as the bookmark it is", async () => {
|
|
vi.spyOn(api, "addToPlaylist").mockResolvedValue({ ok: true } as never);
|
|
vi.spyOn(api, "removeFromPlaylist").mockResolvedValue({ ok: true } as never);
|
|
const qc = new QueryClient();
|
|
await addPlaylistItem(qc, WATCH_LATER, "X");
|
|
expect(marks(qc)).toEqual({ X: { saved: true } });
|
|
await removePlaylistItem(qc, WATCH_LATER, "X");
|
|
expect(marks(qc)).toEqual({ X: { saved: false } });
|
|
});
|
|
|
|
it("marks nothing for an item of any other list", async () => {
|
|
vi.spyOn(api, "addToPlaylist").mockResolvedValue({ ok: true } as never);
|
|
vi.spyOn(api, "removeFromPlaylist").mockResolvedValue({ ok: true } as never);
|
|
const qc = new QueryClient();
|
|
await addPlaylistItem(qc, MINE, "X");
|
|
await removePlaylistItem(qc, { id: 3, kind: undefined }, "X");
|
|
expect(marks(qc)).toBeUndefined();
|
|
});
|
|
|
|
it("marks a move into and out of the Watch later list", async () => {
|
|
vi.spyOn(api, "moveToPlaylist").mockResolvedValue({ ok: true } as never);
|
|
const qc = new QueryClient();
|
|
await movePlaylistItem(qc, MINE, "X", WATCH_LATER);
|
|
expect(marks(qc)).toEqual({ X: { saved: true } });
|
|
await movePlaylistItem(qc, WATCH_LATER, "X", MINE);
|
|
expect(marks(qc)).toEqual({ X: { saved: false } });
|
|
});
|
|
|
|
it("marks nothing the server refused", async () => {
|
|
vi.spyOn(api, "setState").mockRejectedValue(new Error("offline"));
|
|
const qc = new QueryClient();
|
|
await expect(writeVideoState(qc, "X", "hidden")).rejects.toThrow("offline");
|
|
expect(marks(qc)).toBeUndefined();
|
|
});
|
|
});
|