Files
siftlode/frontend/src/lib/playerModalHooks.apiload.dom.test.tsx
T
peter 4b5b66343d 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.
2026-10-25 02:50:34 +02:00

86 lines
3.3 KiB
TypeScript

// @vitest-environment jsdom
//
// The YouTube IFrame API script failing to load must END in an error state. `loadYouTubeApi` used to
// return a promise that only ever resolved: a blocked or failed script (a content blocker, no
// network) left the modal on an empty stage forever, with no error and no way to retry short of a
// reload — the promise was memoised, so every later open waited on the same dead one.
//
// Its own file on purpose: `loadYouTubeApi` memoises the API promise in a module-level singleton,
// and these tests need a module instance that has never seen `window.YT`.
import { describe, it, expect, vi, afterEach } from "vitest";
import { renderHook } from "@testing-library/react";
import { QueryClient } from "@tanstack/react-query";
import { createRef, type RefObject } from "react";
import { useYtProgress, YT_API_LOAD_FAILED, YT_API_TIMEOUT_MS } from "./playerModalHooks";
import type { YTPlayer } from "./youtube";
const VID = "EC5oFYy9z9g";
const apiScripts = () =>
[...document.head.querySelectorAll("script")].filter((s) => s.src.includes("iframe_api"));
const settle = async () => {
for (let i = 0; i < 6; i++) await Promise.resolve();
};
function mountPlayer(setPlayerError: (code: number | null) => void) {
const mount = document.createElement("div");
document.body.appendChild(mount);
return renderHook(() =>
useYtProgress({
activeId: VID,
resumeAt: 0,
exactStart: false,
mountRef: { current: mount },
playerRef: createRef<YTPlayer>() as RefObject<YTPlayer | null>,
currentIdRef: { current: VID },
navRef: { current: { queue: undefined, index: 0 } },
modeRef: { current: { autoMode: "off", loopMode: "off" } },
setWatched: () => {},
setPlayerError,
setLiveData: () => {},
reapplyStored: () => {},
reclaimFocus: () => {},
advanceOnEnd: () => {},
sessionPosRef: { current: new Map() },
qc: new QueryClient(),
}),
);
}
describe("useYtProgress — the IFrame API failing to load", () => {
// Fail any load still in flight, so the module's memoised promise is cleared for the next test
// (a pending one would carry a timer from before that test's fake clock).
afterEach(async () => {
for (const s of apiScripts()) s.dispatchEvent(new Event("error"));
await settle();
vi.useRealTimers();
});
it("shows the load-failed state when the script errors, and a later open tries again", async () => {
const setPlayerError = vi.fn();
const first = mountPlayer(setPlayerError);
const [tag] = apiScripts();
expect(tag).toBeDefined();
tag!.dispatchEvent(new Event("error"));
await settle();
expect(setPlayerError).toHaveBeenCalledWith(YT_API_LOAD_FAILED);
first.unmount();
// Not memoised as a failure: the next open loads the script again rather than waiting on the
// dead promise.
const again = mountPlayer(vi.fn());
expect(apiScripts()).toHaveLength(1);
again.unmount();
});
it("gives up after the timeout when the script never answers", async () => {
vi.useFakeTimers();
const setPlayerError = vi.fn();
const { unmount } = mountPlayer(setPlayerError);
await vi.advanceTimersByTimeAsync(YT_API_TIMEOUT_MS + 1);
expect(setPlayerError).toHaveBeenCalledWith(YT_API_LOAD_FAILED);
unmount();
});
});