fix(nav): a linked player, file and search survive what the app does by itself (review round 2)

- Playlists: a link over a playlist the account does not hold plays alone; the rail's own
  fallback no longer counts as a pick that gives the player up.
- The linked opener opens or clears nothing once the address names another player, and stops
  looking (and fetching its row) once it settled.
- Files: a list the server's cap cut — the sorted or searched one too — keeps the link, with a
  notice of its own (EN/HU).
- History: an in-place view rewrite (search refine, count, channel to channel, sub-view)
  drops a reloaded entry's overlay marks (replaceView); a landing on a channel opened from a
  search keeps the search; the Feed press on a search opens the plain feed.
- Files/Playlists: an emptied search is written at once.
- Client log: plf/cf (column text filters) are typed keys; the shell has none of its own.
- Contract: writers outside lib/ take a literal or a lib/ function's result; every URL key is
  sorted typed or not; the writer regexes are held by a fixture; sub-views named; the bookmark
  net reads page tags.
- Feed defaults in one constant; one live-search queue helper; Scheduler writes through the
  Activity codec; notRun copy (EN/HU) names Forward and a plain "Search YouTube" button.
This commit is contained in:
2026-10-27 16:08:52 +01:00
parent 0d5c6273f1
commit 6d57d62ac8
38 changed files with 717 additions and 187 deletions
@@ -83,6 +83,60 @@ describe("useOpenLinkedPlayer", () => {
expect(new URLSearchParams(window.location.search).has("watch")).toBe(false);
});
it("leaves a player opened while the row was on its way alone, and its address", async () => {
window.history.replaceState(null, "", "/?watch=Z");
let answer: (v: Video) => void = () => {};
vi.spyOn(api, "videoRow").mockReturnValue(new Promise((r) => (answer = r)));
const open = vi.fn();
renderHook(() => useOpenLinkedPlayer(true, () => undefined, open), { wrapper });
// A card clicked meanwhile: its player names itself in the address.
window.history.replaceState(null, "", "/?watch=Y");
answer(vid("Z"));
await new Promise((r) => setTimeout(r, 20));
expect(open).not.toHaveBeenCalled();
expect(new URLSearchParams(window.location.search).get("watch")).toBe("Y");
});
it("does not clear the address of a player opened since when its catalog read fails", async () => {
window.history.replaceState(null, "", "/?watch=Z");
let refuse: (e: Error) => void = () => {};
vi.spyOn(api, "videoRow").mockReturnValue(new Promise((_, r) => (refuse = r)));
renderHook(() => useOpenLinkedPlayer(true, () => undefined, vi.fn()), { wrapper });
window.history.replaceState(null, "", "/?watch=Y");
refuse(new Error("not found"));
await new Promise((r) => setTimeout(r, 20));
expect(new URLSearchParams(window.location.search).get("watch")).toBe("Y");
});
it("given up over a moved list, keeps the address of a player opened since", async () => {
window.history.replaceState(null, "", "/?watch=Z");
const { rerender } = renderHook(
({ scope }) => useOpenLinkedPlayer(false, () => undefined, vi.fn(), scope),
{ wrapper, initialProps: { scope: 1 } },
);
window.history.replaceState(null, "", "/?watch=Y");
rerender({ scope: 2 });
await new Promise((r) => setTimeout(r, 20));
expect(new URLSearchParams(window.location.search).get("watch")).toBe("Y");
});
it("stops looking once it opened: the video leaving the list asks the catalog for nothing", async () => {
window.history.replaceState(null, "", "/?watch=B");
const row = vi.spyOn(api, "videoRow").mockResolvedValue(vid("B"));
const find = vi.fn((id: string) => (id === "B" ? vid("B") : undefined));
const { rerender } = renderHook(
({ rows }) => useOpenLinkedPlayer(true, rows ? find : () => undefined, vi.fn()),
{ wrapper, initialProps: { rows: true } },
);
await waitFor(() => expect(find).toHaveBeenCalled());
find.mockClear();
rerender({ rows: false });
rerender({ rows: true });
await new Promise((r) => setTimeout(r, 20));
expect(find).not.toHaveBeenCalled();
expect(row).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");