fix(player): related marks survive, Restart restarts, and refused pages offer nothing to click

- Every state and Watch-later write goes through features/video/videoWrites,
  which marks the never-refetched lists (related lists, live searches) once
  the server has it; a contract test holds it. The Notification Center's
  undo, the feed's reset and the Watch-later toggles skipped it before.
- Confirmed marks are also kept apart (qk.videoMarks) and laid over the
  related rows: a Show more page landing later rebuilt them from the pages
  it read when it started, and dropped a mark made meanwhile.
- Restart on a related video starts at 0 (the session's stored position
  won before), and on the one already playing it seeks.
- The watched toggle remembers each video's mark: Back showed the opened
  video's launch-time state after a related one was marked.
- The help sheet leaves out the player's keys while a layer above it has
  them waiting.
- After a refusal a retry cannot answer, neither Show more (related) nor
  Load more (live search) is offered; a failed live-search page keeps the
  results already read and says why under them.
- The related cap message counts requests, as the cap does.
This commit is contained in:
2026-10-25 02:50:34 +02:00
parent ed51f5be7e
commit 4b5b66343d
26 changed files with 667 additions and 93 deletions
@@ -0,0 +1,47 @@
// @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 { resetVideoState, writeVideoState, writeWatchLater } from "./videoWrites";
afterEach(() => vi.restoreAllMocks());
const marks = (qc: QueryClient) => qc.getQueryData(qk.videoMarks());
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 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();
});
});