// @vitest-environment jsdom // // The basic keys of a player on the browser's own media element (the library's, a shared link's): // Space/k play and pause — but Space on a focused button presses the button — the arrows step 5 s // inside the item, m mutes, f fills the screen with a video (an audio file has no picture). While the // element itself has the focus, Space and the arrows are its own controls' (they act on them anyway, // so a second action from here was a double play/pause and a double step). import { describe, it, expect, vi, afterEach } from "vitest"; import { renderHook, cleanup } from "@testing-library/react"; const fs = vi.hoisted(() => ({ requestFullscreen: vi.fn(), exitFullscreen: vi.fn() })); vi.mock("./fullscreen", () => ({ fullscreenElement: () => null, requestFullscreen: fs.requestFullscreen, exitFullscreen: fs.exitFullscreen, })); import { MEDIA_STEP_S, useMediaKeys } from "./mediaKeys"; afterEach(() => { cleanup(); vi.clearAllMocks(); document.body.innerHTML = ""; }); function media(tag: "video" | "audio" = "video", duration = 100) { const el = document.createElement(tag); document.body.append(el); let paused = true; Object.defineProperty(el, "duration", { value: duration, configurable: true }); Object.defineProperty(el, "paused", { get: () => paused, configurable: true }); vi.spyOn(el, "play").mockImplementation(() => { paused = false; return Promise.resolve(); }); vi.spyOn(el, "pause").mockImplementation(() => { paused = true; }); return el; } function press(key: string, target: EventTarget = document.body): KeyboardEvent { const e = new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true }); target.dispatchEvent(e); return e; } describe("useMediaKeys", () => { it("Space and k play and pause, and take the key from the page", () => { const el = media(); renderHook(() => useMediaKeys(el)); const e = press(" "); expect(el.play).toHaveBeenCalledOnce(); expect(e.defaultPrevented).toBe(true); press("k"); expect(el.pause).toHaveBeenCalledOnce(); }); it("on the focused element, Space and the arrows are left to its own controls; k still is ours", () => { const el = media("video", 100); renderHook(() => useMediaKeys(el)); el.currentTime = 50; const space = press(" ", el); const arrow = press("ArrowRight", el); expect(el.play).not.toHaveBeenCalled(); expect(el.currentTime).toBe(50); expect([space.defaultPrevented, arrow.defaultPrevented]).toEqual([false, false]); press("k", el); expect(el.play).toHaveBeenCalledOnce(); }); it("Space on a focused button presses the button instead", () => { const el = media(); const button = document.createElement("button"); document.body.append(button); renderHook(() => useMediaKeys(el)); const e = press(" ", button); expect(el.play).not.toHaveBeenCalled(); expect(e.defaultPrevented).toBe(false); }); it("the arrows step inside the item", () => { const el = media("video", 100); renderHook(() => useMediaKeys(el)); el.currentTime = 2; press("ArrowLeft"); expect(el.currentTime).toBe(0); el.currentTime = 50; press("ArrowRight"); expect(el.currentTime).toBe(50 + MEDIA_STEP_S); el.currentTime = 98; press("ArrowRight"); expect(el.currentTime).toBe(100); }); it("m mutes and unmutes", () => { const el = media(); renderHook(() => useMediaKeys(el)); press("m"); expect(el.muted).toBe(true); press("m"); expect(el.muted).toBe(false); }); it("f fills the screen with a video, and leaves an audio file alone", () => { const v = media("video"); const first = renderHook(() => useMediaKeys(v)); press("f"); expect(fs.requestFullscreen).toHaveBeenCalledWith(v); first.unmount(); const a = media("audio"); renderHook(() => useMediaKeys(a)); const e = press("f"); expect(fs.requestFullscreen).toHaveBeenCalledOnce(); expect(e.defaultPrevented).toBe(false); }); it("stays out of a text field, and goes with the player", () => { const el = media(); const input = document.createElement("input"); document.body.append(input); const { unmount } = renderHook(() => useMediaKeys(el)); press(" ", input); expect(el.play).not.toHaveBeenCalled(); unmount(); press(" "); expect(el.play).not.toHaveBeenCalled(); }); });