feat(ai-character): 관리자 인증 셸 구현
This commit is contained in:
117
src/shared/ui/__tests__/admin-audio-player.test.tsx
Normal file
117
src/shared/ui/__tests__/admin-audio-player.test.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { fireEvent, render, screen } 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: "볼륨" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("combobox", { name: "재생 속도" })).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();
|
||||
});
|
||||
Reference in New Issue
Block a user