Files
voiceon-character-admin/src/shared/ui/__tests__/admin-audio-player.test.tsx

142 lines
6.6 KiB
TypeScript

import { fireEvent, render, screen, within } from "@testing-library/react";
import { afterEach, beforeEach, expect, test, vi } from "vitest";
import { AdminAudioPlayer } from "@/shared/ui/admin-audio-player";
import { AudioPlaybackProvider } from "@/shared/ui/audio-playback-provider";
let playSpy: ReturnType<typeof vi.spyOn>;
let pauseSpy: ReturnType<typeof vi.spyOn>;
let loadSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
playSpy = vi.spyOn(HTMLMediaElement.prototype, "play").mockResolvedValue(undefined);
pauseSpy = vi.spyOn(HTMLMediaElement.prototype, "pause").mockImplementation(() => undefined);
loadSpy = vi.spyOn(HTMLMediaElement.prototype, "load").mockImplementation(() => undefined);
});
afterEach(() => {
playSpy.mockRestore();
pauseSpy.mockRestore();
loadSpy.mockRestore();
});
test("AdminAudioPlayer wraps native audio with controls and no download or autoplay", () => {
render(<AdminAudioPlayer playerId="one" src="https://cdn.example.com/signed/audio.m4a?token=secret" title="샘플 오디오" />);
const audio = document.querySelector("audio");
expect(audio).toHaveAttribute("src", "https://cdn.example.com/signed/audio.m4a?token=secret");
expect(audio).not.toHaveAttribute("autoplay");
expect(audio).toHaveAttribute("controlsList", "nodownload");
expect(screen.getByRole("button", { name: "재생" })).toBeInTheDocument();
expect(screen.getByRole("slider", { name: "재생 위치" })).toBeInTheDocument();
expect(screen.getByRole("slider", { name: "볼륨" })).toHaveValue("1");
expect(screen.getByRole("combobox", { name: "재생 속도" })).toBeInTheDocument();
});
test("AdminAudioPlayer matches the compact reference control anatomy", () => {
render(<AdminAudioPlayer playerId="one" src="https://cdn.example.com/signed/audio.m4a?token=secret" title="샘플 오디오" />);
const player = screen.getByRole("group", { name: "샘플 오디오 오디오 플레이어" });
const controls = within(player).getByRole("group", { name: "재생 제어" });
expect(within(controls).getByRole("button", { name: "재생" })).toBeInTheDocument();
const seek = within(controls).getByRole("slider", { name: "재생 위치" });
const speed = within(controls).getByRole("combobox", { name: "재생 속도" });
const volume = within(controls).getByRole("slider", { name: "볼륨" });
expect.soft(player).toHaveClass("px-1", "sm:px-3");
expect(seek).toHaveClass("min-w-11");
expect.soft(speed).toHaveClass("w-11");
expect(speed).toHaveClass("text-base");
expect.soft(volume).toHaveClass("basis-11");
expect(volume).toHaveClass("min-w-11");
expect(volume).toBeInTheDocument();
expect(volume.className).not.toContain("hidden");
expect.soft(within(controls).queryByText("0:00/0:00")).toBeInTheDocument();
expect(within(player).queryByText("볼륨")).not.toBeInTheDocument();
expect(within(player).queryByText("재생 속도")).not.toBeInTheDocument();
expect(player.querySelector("img")).not.toBeInTheDocument();
expect(player.querySelector("video")).not.toBeInTheDocument();
});
test("AudioPlaybackProvider keeps only one player active by player id", () => {
render(
<AudioPlaybackProvider>
<AdminAudioPlayer playerId="one" src="https://cdn.example.com/signed/one.m4a?token=secret-one" title="첫 번째" />
<AdminAudioPlayer playerId="two" src="https://cdn.example.com/signed/two.m4a?token=secret-two" title="두 번째" />
</AudioPlaybackProvider>,
);
const playButtons = screen.getAllByRole("button", { name: "재생" });
const firstPlayButton = playButtons[0];
const secondPlayButton = playButtons[1];
if (firstPlayButton === undefined || secondPlayButton === undefined) {
throw new Error("expected two play buttons");
}
fireEvent.click(firstPlayButton);
fireEvent.click(secondPlayButton);
expect(playSpy).toHaveBeenCalledTimes(2);
expect(pauseSpy).toHaveBeenCalled();
});
test("AdminAudioPlayer supports keyboard play, seek, volume, speed, generic error, and manual retry only", () => {
render(<AdminAudioPlayer playerId="one" src="https://cdn.example.com/signed/audio.m4a?token=secret" title="샘플 오디오" />);
const player = screen.getByRole("group", { name: "샘플 오디오 오디오 플레이어" });
fireEvent.keyDown(player, { key: " " });
fireEvent.change(screen.getByRole("slider", { name: "재생 위치" }), { target: { value: "12" } });
fireEvent.change(screen.getByRole("slider", { name: "볼륨" }), { target: { value: "0.5" } });
fireEvent.change(screen.getByRole("combobox", { name: "재생 속도" }), { target: { value: "1.5" } });
const audio = document.querySelector("audio");
if (!(audio instanceof HTMLAudioElement)) {
throw new Error("expected native audio element");
}
fireEvent.error(audio);
expect(playSpy).toHaveBeenCalledTimes(1);
expect(screen.getByRole("alert")).toHaveTextContent("오디오를 재생할 수 없습니다");
fireEvent.click(screen.getByRole("button", { name: "오디오 다시 시도" }));
expect(loadSpy).toHaveBeenCalledTimes(1);
expect(playSpy).toHaveBeenCalledTimes(1);
});
test("AdminAudioPlayer ignores Enter and Space from descendant controls", () => {
render(<AdminAudioPlayer playerId="one" src="https://cdn.example.com/signed/audio.m4a?token=secret" title="샘플 오디오" />);
fireEvent.keyDown(screen.getByRole("combobox", { name: "재생 속도" }), { key: "Enter" });
fireEvent.keyDown(screen.getByRole("button", { name: "재생" }), { key: " " });
expect(playSpy).not.toHaveBeenCalled();
});
test("AudioPlaybackProvider and AdminAudioPlayer do not log or persist signed URLs", () => {
const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
const localStorageSpy = vi.spyOn(Storage.prototype, "setItem");
const signedUrl = "https://cdn.example.com/signed/audio.m4a?token=secret";
render(
<AudioPlaybackProvider>
<AdminAudioPlayer playerId="one" src={signedUrl} title="샘플 오디오" />
</AudioPlaybackProvider>,
);
fireEvent.click(screen.getByRole("button", { name: "재생" }));
const audio = document.querySelector("audio");
if (!(audio instanceof HTMLAudioElement)) {
throw new Error("expected native audio element");
}
fireEvent.error(audio);
expect(logSpy).not.toHaveBeenCalled();
expect(warnSpy).not.toHaveBeenCalled();
expect(errorSpy).not.toHaveBeenCalled();
expect(localStorageSpy).not.toHaveBeenCalledWith(expect.any(String), expect.stringContaining(signedUrl));
logSpy.mockRestore();
warnSpy.mockRestore();
errorSpy.mockRestore();
localStorageSpy.mockRestore();
});