The reported bug: after F, the embed's own fullscreen button is dead. Root cause
measured in Chrome — the browser's fullscreen lock sits on OUR wrapper, so
YouTube's button would need a nested request from a cross-origin frame. Handing
the lock to the iframe fixes it, but `iframe.requestFullscreen()` only resolves
from a CLICK (from a key press the promise stays pending forever, and awaiting
it spends the activation so a fallback is denied too).
So: F keeps fullscreening our wrapper (unchanged, our overlays stay visible) and
a new toolbar button hands the lock to the iframe — verified in real Chrome:
document.fullscreenElement is the IFRAME at 1920x1024, i.e. a plain YouTube
fullscreen where its own controls work.
Round-6 review findings, all fixed:
- one _rate_limiter + one _cancel_poll shared by the yt-dlp hook and run_ffmpeg
(was three hand-rolled throttles; cancel latency differed 2s vs 1s)
- stepVolume takes {delta, fallback} — two adjacent number args were silently
transposable
- volume gestures before the player is ready are honoured instead of dropped
- normalizeVolume's fallback is typed unknown (its guard was unreachable) and
applyVolume takes a number again
- the throttle tests inject a clock instead of patching global time.monotonic
59 lines
2.2 KiB
TypeScript
59 lines
2.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { DEFAULT_VOLUME, normalizeVolume, stepVolume } from "./playerVolume";
|
|
|
|
describe("normalizeVolume", () => {
|
|
it("keeps a valid level", () => {
|
|
expect(normalizeVolume(0)).toBe(0);
|
|
expect(normalizeVolume(37)).toBe(37);
|
|
expect(normalizeVolume(100)).toBe(100);
|
|
});
|
|
|
|
it("clamps out-of-range levels and rounds to whole percents", () => {
|
|
expect(normalizeVolume(-20)).toBe(0);
|
|
expect(normalizeVolume(150)).toBe(100);
|
|
expect(normalizeVolume(42.6)).toBe(43);
|
|
});
|
|
|
|
it.each([
|
|
["a missing key", undefined],
|
|
["an explicit null", null],
|
|
["a stringified number", "80"],
|
|
["NaN from a broken player", NaN],
|
|
["a whole object", { volume: 50 }],
|
|
])("falls back on %s", (_label, raw) => {
|
|
// The regression: a partial stored object made the level undefined, which Math.round turned
|
|
// into NaN — muting playback and persisting `null` for every later session.
|
|
expect(normalizeVolume(raw, 15)).toBe(15);
|
|
});
|
|
|
|
it("prefers the last known level over the default, so a quiet session stays quiet", () => {
|
|
expect(normalizeVolume(NaN, 15)).toBe(15);
|
|
expect(normalizeVolume(NaN, 15)).not.toBe(DEFAULT_VOLUME);
|
|
});
|
|
|
|
it("uses the default when the fallback is unusable too", () => {
|
|
expect(normalizeVolume(NaN, NaN)).toBe(DEFAULT_VOLUME);
|
|
expect(normalizeVolume(null, undefined)).toBe(DEFAULT_VOLUME);
|
|
expect(normalizeVolume(null, "80")).toBe(DEFAULT_VOLUME);
|
|
});
|
|
});
|
|
|
|
describe("stepVolume", () => {
|
|
it("steps up and down from the current level", () => {
|
|
expect(stepVolume(60, { delta: 5, fallback: 100 })).toBe(65);
|
|
expect(stepVolume(60, { delta: -5, fallback: 100 })).toBe(55);
|
|
});
|
|
|
|
it("clamps at both ends instead of wrapping", () => {
|
|
expect(stepVolume(98, { delta: 5, fallback: 100 })).toBe(100);
|
|
expect(stepVolume(3, { delta: -5, fallback: 100 })).toBe(0);
|
|
});
|
|
|
|
it("steps from the fallback when the player answers garbage", () => {
|
|
// A spin must not lose notches to a stale/unusable reading — it continues from what we last set.
|
|
expect(stepVolume(undefined, { delta: -5, fallback: 90 })).toBe(85);
|
|
expect(stepVolume(NaN, { delta: -5, fallback: 90 })).toBe(85);
|
|
});
|
|
});
|