142 lines
6.6 KiB
TypeScript
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();
|
|
});
|