feat(ai-character): 관리자 인증 셸 구현

This commit is contained in:
Yu Sung
2026-07-27 15:04:03 +09:00
parent ba43dd829e
commit 5356bd1e6a
82 changed files with 6017 additions and 18 deletions

View 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();
});