/** * @file AudioPlayer.test.tsx * @description Tests for the AudioPlayer component that provides inline TTS audio playback */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { AudioPlayer } from "./AudioPlayer"; // Mock HTMLAudioElement class MockAudio { src = ""; currentTime = 0; duration = 60; paused = true; playbackRate = 1; volume = 1; onended: (() => void) | null = null; ontimeupdate: (() => void) | null = null; onloadedmetadata: (() => void) | null = null; onerror: ((e: unknown) => void) | null = null; play(): Promise { this.paused = false; return Promise.resolve(); } pause(): void { this.paused = true; } addEventListener(event: string, handler: () => void): void { if (event === "ended") this.onended = handler; if (event === "timeupdate") this.ontimeupdate = handler; if (event === "loadedmetadata") this.onloadedmetadata = handler; if (event === "error") this.onerror = handler; } removeEventListener(): void { // no-op for tests } } vi.stubGlobal("Audio", MockAudio); describe("AudioPlayer", () => { beforeEach(() => { vi.clearAllMocks(); }); describe("rendering", () => { it("should render play button", () => { render(); const playButton = screen.getByRole("button", { name: "Play audio" }); expect(playButton).toBeInTheDocument(); }); it("should render download button", () => { render(); const downloadButton = screen.getByRole("button", { name: /download/i }); expect(downloadButton).toBeInTheDocument(); }); it("should render time display showing 0:00", () => { render(); expect(screen.getByText("0:00")).toBeInTheDocument(); }); it("should render speed control", () => { render(); const speedButton = screen.getByRole("button", { name: "Playback speed" }); expect(speedButton).toBeInTheDocument(); }); it("should render progress bar", () => { render(); const progressBar = screen.getByRole("progressbar"); expect(progressBar).toBeInTheDocument(); }); it("should not render when src is null", () => { const { container } = render(); expect(container.firstChild).toBeNull(); }); }); describe("play/pause", () => { it("should toggle to pause button when playing", async () => { const user = userEvent.setup(); render(); const playButton = screen.getByRole("button", { name: "Play audio" }); await user.click(playButton); expect(screen.getByRole("button", { name: "Pause audio" })).toBeInTheDocument(); }); }); describe("speed control", () => { it("should cycle through speed options on click", async () => { const user = userEvent.setup(); render(); const speedButton = screen.getByRole("button", { name: "Playback speed" }); // Default should be 1x expect(speedButton).toHaveTextContent("1x"); // Click to go to 1.5x await user.click(speedButton); expect(speedButton).toHaveTextContent("1.5x"); // Click to go to 2x await user.click(speedButton); expect(speedButton).toHaveTextContent("2x"); // Click to go to 0.5x await user.click(speedButton); expect(speedButton).toHaveTextContent("0.5x"); // Click to go back to 1x await user.click(speedButton); expect(speedButton).toHaveTextContent("1x"); }); }); describe("accessibility", () => { it("should have proper aria labels on controls", () => { render(); expect(screen.getByRole("button", { name: "Play audio" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /download/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Playback speed" })).toBeInTheDocument(); expect(screen.getByRole("progressbar")).toHaveAttribute("aria-label"); }); it("should have region role on the player container", () => { render(); expect(screen.getByRole("region", { name: /audio player/i })).toBeInTheDocument(); }); }); describe("design", () => { it("should not use aggressive red colors", () => { const { container } = render(); const allElements = container.querySelectorAll("*"); allElements.forEach((el) => { const className = el.className; if (typeof className === "string") { expect(className).not.toMatch(/bg-red-|text-red-|border-red-/); } }); }); }); describe("callbacks", () => { it("should call onPlayStateChange when play state changes", async () => { const onPlayStateChange = vi.fn(); const user = userEvent.setup(); render(); const playButton = screen.getByRole("button", { name: "Play audio" }); await user.click(playButton); expect(onPlayStateChange).toHaveBeenCalledWith(true); }); }); });