import { describe, expect, test, vi } from "vitest"; import { act, fireEvent, screen, waitFor } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { MediaPlayer } from "../../../components/player/MediaPlayer.tsx"; import { renderWithContexts } from "../../render.tsx"; function media(container: HTMLElement) { return container.querySelector("audio, video")!; } function setTime(el: HTMLMediaElement, currentTime: number, duration: number) { Object.defineProperty(el, "duration", { configurable: true, value: duration, }); el.currentTime = currentTime; fireEvent(el, new Event("durationchange")); fireEvent(el, new Event("timeupdate")); } describe("MediaPlayer", () => { test("play and pause through the button, reporting state", async () => { const onPlayStateChange = vi.fn(); const play = vi.spyOn(HTMLMediaElement.prototype, "play"); const pause = vi.spyOn(HTMLMediaElement.prototype, "pause"); renderWithContexts( , ); await userEvent.click(screen.getByRole("button", { name: "Play" })); expect(play).toHaveBeenCalled(); expect(onPlayStateChange).toHaveBeenLastCalledWith(true); await userEvent.click(screen.getByRole("button", { name: "Pause" })); expect(pause).toHaveBeenCalled(); expect(onPlayStateChange).toHaveBeenLastCalledWith(false); }); test("autoplays when asked", async () => { const onPlayStateChange = vi.fn(); renderWithContexts( , ); await waitFor(() => expect(onPlayStateChange).toHaveBeenCalledWith(true)); expect(screen.getByRole("button", { name: "Pause" })).toBeInTheDocument(); }); test("a play refused by the browser stays paused", async () => { vi.spyOn(HTMLMediaElement.prototype, "play").mockRejectedValue( new Error("NotAllowedError"), ); renderWithContexts( , ); await act(async () => {}); expect(screen.getByRole("button", { name: "Play" })).toBeInTheDocument(); }); test("a pause from outside the page (a media key) is reflected", async () => { const onPlayStateChange = vi.fn(); const { container } = renderWithContexts( , ); await userEvent.click(screen.getByRole("button", { name: "Play" })); fireEvent(media(container), new Event("pause")); expect(screen.getByRole("button", { name: "Play" })).toBeInTheDocument(); expect(onPlayStateChange).toHaveBeenLastCalledWith(false); fireEvent(media(container), new Event("play")); expect(screen.getByRole("button", { name: "Pause" })).toBeInTheDocument(); expect(onPlayStateChange).toHaveBeenLastCalledWith(true); }); test("time, duration, seeking and the end", async () => { const onTimeUpdate = vi.fn(); const onEnded = vi.fn(); const { container } = renderWithContexts( , ); const el = media(container); setTime(el, 65, 200); expect(screen.getByText("1:05")).toBeInTheDocument(); expect(screen.getByText("3:20")).toBeInTheDocument(); expect(onTimeUpdate).toHaveBeenLastCalledWith(65, 200); fireEvent.change(screen.getByRole("slider", { name: "Seek" }), { target: { value: "120" }, }); expect(el.currentTime).toBe(120); fireEvent(el, new Event("ended")); expect(onEnded).toHaveBeenCalled(); }); test("while the seek bar is held, playback doesn't move the thumb — by touch too", () => { const { container } = renderWithContexts( , ); const el = media(container); setTime(el, 10, 100); const seek = screen.getByRole("slider", { name: "Seek" }); fireEvent.pointerDown(seek, { pointerType: "touch" }); fireEvent.change(seek, { target: { value: "50" } }); setTime(el, 11, 100); expect(screen.getByText("0:50")).toBeInTheDocument(); fireEvent.pointerUp(seek, { pointerType: "touch" }); setTime(el, 51, 100); expect(screen.getByText("0:51")).toBeInTheDocument(); }); test("an error is reported for re-resolving", () => { const onError = vi.fn(); const { container } = renderWithContexts( , ); fireEvent(media(container), new Event("error")); expect(onError).toHaveBeenCalled(); }); test("volume and mute are remembered", async () => { const { container } = renderWithContexts( , ); fireEvent.change(screen.getByRole("slider", { name: "Volume" }), { target: { value: "0.4" }, }); expect(media(container).volume).toBeCloseTo(0.4); await userEvent.click(screen.getByRole("button", { name: "Mute" })); expect(media(container).muted).toBe(true); expect(JSON.parse(localStorage.getItem("player-volume")!)).toEqual({ volume: 0.4, muted: true, }); expect(screen.getByRole("slider", { name: "Volume" })).toHaveValue("0"); // Raising the volume unmutes. fireEvent.change(screen.getByRole("slider", { name: "Volume" }), { target: { value: "0.7" }, }); expect(media(container).muted).toBe(false); }); test("registers seek and toggle handles for the global player", async () => { const seekRef = { current: null as ((t: number) => void) | null }; const toggleRef = { current: null as (() => void) | null }; const { container, unmount } = renderWithContexts( , ); act(() => seekRef.current!(33)); expect(media(container).currentTime).toBe(33); await act(async () => toggleRef.current!()); expect(screen.getByRole("button", { name: "Pause" })).toBeInTheDocument(); unmount(); expect(seekRef.current).toBeNull(); }); test("resumes at a start offset once metadata is in", () => { const { container } = renderWithContexts( , ); fireEvent(media(container), new Event("loadedmetadata")); expect(media(container).currentTime).toBe(42); }); test("video has a fullscreen button", () => { renderWithContexts(); expect(screen.getByRole("button", { name: "Fullscreen" })) .toBeInTheDocument(); }); });