Turning ready, the pointer leaving the video, the window regaining focus, a wheel over it and leaving fullscreen each put the focus back on the card unconditionally, so a viewer who had tabbed to a control right after opening started over from the first button on the next Tab. They now take it back only when it sits in the iframe or off the card.
85 lines
3.2 KiB
TypeScript
85 lines
3.2 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,
|
|
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();
|
|
});
|
|
});
|