207 lines
7.1 KiB
TypeScript
207 lines
7.1 KiB
TypeScript
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<HTMLMediaElement>("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(
|
|
<MediaPlayer
|
|
src="https://s/a.mp3"
|
|
kind="audio"
|
|
trackStyle="progress"
|
|
onPlayStateChange={onPlayStateChange}
|
|
/>,
|
|
);
|
|
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(
|
|
<MediaPlayer
|
|
src="https://s/a.mp3"
|
|
kind="audio"
|
|
trackStyle="progress"
|
|
autoplay
|
|
onPlayStateChange={onPlayStateChange}
|
|
/>,
|
|
);
|
|
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(
|
|
<MediaPlayer
|
|
src="https://s/a.mp3"
|
|
kind="audio"
|
|
trackStyle="progress"
|
|
autoplay
|
|
/>,
|
|
);
|
|
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(
|
|
<MediaPlayer
|
|
src="https://s/a.mp3"
|
|
kind="audio"
|
|
trackStyle="progress"
|
|
onPlayStateChange={onPlayStateChange}
|
|
/>,
|
|
);
|
|
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(
|
|
<MediaPlayer
|
|
src="https://s/a.mp3"
|
|
kind="audio"
|
|
trackStyle="progress"
|
|
onTimeUpdate={onTimeUpdate}
|
|
onEnded={onEnded}
|
|
/>,
|
|
);
|
|
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(
|
|
<MediaPlayer src="https://s/a.mp3" kind="audio" trackStyle="progress" />,
|
|
);
|
|
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(
|
|
<MediaPlayer
|
|
src="https://s/a.mp3"
|
|
kind="audio"
|
|
trackStyle="progress"
|
|
onError={onError}
|
|
/>,
|
|
);
|
|
fireEvent(media(container), new Event("error"));
|
|
expect(onError).toHaveBeenCalled();
|
|
});
|
|
|
|
test("volume and mute are remembered", async () => {
|
|
const { container } = renderWithContexts(
|
|
<MediaPlayer src="https://s/a.mp3" kind="audio" trackStyle="progress" />,
|
|
);
|
|
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(
|
|
<MediaPlayer
|
|
src="https://s/a.mp3"
|
|
kind="audio"
|
|
trackStyle="progress"
|
|
seekRef={seekRef}
|
|
toggleRef={toggleRef}
|
|
/>,
|
|
);
|
|
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(
|
|
<MediaPlayer
|
|
src="https://s/a.mp3"
|
|
kind="audio"
|
|
trackStyle="progress"
|
|
startTime={42}
|
|
/>,
|
|
);
|
|
fireEvent(media(container), new Event("loadedmetadata"));
|
|
expect(media(container).currentTime).toBe(42);
|
|
});
|
|
|
|
test("video has a fullscreen button", () => {
|
|
renderWithContexts(<MediaPlayer src="https://s/a.mp4" kind="video" />);
|
|
expect(screen.getByRole("button", { name: "Fullscreen" }))
|
|
.toBeInTheDocument();
|
|
});
|
|
});
|