feat(ai-character): 공통 오디오 플레이어 UI 개선
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
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";
|
||||
@@ -33,6 +33,30 @@ test("AdminAudioPlayer wraps native audio with controls and no download or autop
|
||||
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>
|
||||
|
||||
@@ -87,7 +87,7 @@ export function AdminAudioPlayer({ playerId, src, title }: AdminAudioPlayerProps
|
||||
}
|
||||
|
||||
return (
|
||||
<section aria-label={`${title} 오디오 플레이어`} className="flex min-w-0 flex-col gap-3 rounded-lg border border-border bg-card p-4" onKeyDown={handleKeyDown} role="group" tabIndex={0}>
|
||||
<section aria-label={`${title} 오디오 플레이어`} className="flex min-w-0 flex-col gap-2 rounded-lg border border-border bg-card px-1 py-1 sm:px-3" onKeyDown={handleKeyDown} role="group" tabIndex={0}>
|
||||
<audio
|
||||
controlsList="nodownload"
|
||||
onDurationChange={(event) => setDuration(event.currentTarget.duration)}
|
||||
@@ -106,30 +106,26 @@ export function AdminAudioPlayer({ playerId, src, title }: AdminAudioPlayerProps
|
||||
ref={audioRef}
|
||||
src={src}
|
||||
/>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button className="rounded-md border border-input bg-primary px-4 py-2 font-semibold text-primary-foreground hover:bg-[var(--button-bg-hover)] active:bg-[var(--button-bg-active)]" onClick={togglePlay} type="button">
|
||||
{isPlaying ? "일시정지" : "재생"}
|
||||
<div aria-label="재생 제어" className="flex min-w-0 flex-wrap items-center gap-1 sm:gap-2" role="group">
|
||||
<button aria-label={isPlaying ? "일시정지" : "재생"} className="grid size-11 shrink-0 place-items-center rounded-full border border-input bg-primary text-primary-foreground hover:bg-[var(--button-bg-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 active:bg-[var(--button-bg-active)]" onClick={togglePlay} type="button">
|
||||
<svg aria-hidden="true" className="size-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
{isPlaying ? <path d="M7 5h4v14H7zm6 0h4v14h-4z" /> : <path d="m8 5 11 7-11 7z" />}
|
||||
</svg>
|
||||
</button>
|
||||
<span className="text-sm text-muted-foreground">{formatTime(currentTime)} / {formatTime(duration)}</span>
|
||||
</div>
|
||||
<label className="flex flex-col gap-2 text-sm font-semibold">
|
||||
재생 위치
|
||||
<input aria-label="재생 위치" max={duration || 0} min="0" onChange={(event) => changeCurrentTime(Number(event.currentTarget.value))} step="1" type="range" value={currentTime} />
|
||||
</label>
|
||||
<label className="flex flex-col gap-2 text-sm font-semibold">
|
||||
볼륨
|
||||
<input aria-label="볼륨" defaultValue="1" max="1" min="0" onChange={(event) => changeVolume(Number(event.currentTarget.value))} step="0.05" type="range" />
|
||||
</label>
|
||||
<label className="flex flex-col gap-2 text-sm font-semibold">
|
||||
재생 속도
|
||||
<select aria-label="재생 속도" className="rounded-md border border-input bg-card px-3 py-2 text-base" onChange={(event) => changePlaybackRate(Number(event.currentTarget.value))} defaultValue="1">
|
||||
<input aria-label="재생 위치" className="h-11 min-w-11 flex-1 accent-primary" max={duration || 0} min="0" onChange={(event) => changeCurrentTime(Number(event.currentTarget.value))} step="1" type="range" value={currentTime} />
|
||||
<span className="shrink-0 text-xs tabular-nums text-muted-foreground">{formatTime(currentTime)}/{formatTime(duration)}</span>
|
||||
<select aria-label="재생 속도" className="min-h-11 w-11 shrink-0 rounded-md border border-input bg-card px-1 text-center text-base font-semibold sm:text-sm" defaultValue="1" onChange={(event) => changePlaybackRate(Number(event.currentTarget.value))}>
|
||||
<option value="0.75">0.75×</option>
|
||||
<option value="1">1×</option>
|
||||
<option value="1.25">1.25×</option>
|
||||
<option value="1.5">1.5×</option>
|
||||
<option value="2">2×</option>
|
||||
</select>
|
||||
</label>
|
||||
<svg aria-hidden="true" className="size-4 shrink-0 text-muted-foreground" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M4 9v6h4l5 4V5L8 9zm11.5 3a3.5 3.5 0 0 0-1.5-2.87v5.74A3.5 3.5 0 0 0 15.5 12Zm-1.5-7.46v2.06a6 6 0 0 1 0 10.8v2.06a8 8 0 0 0 0-14.92Z" />
|
||||
</svg>
|
||||
<input aria-label="볼륨" className="h-11 min-w-11 basis-11 shrink grow-0 accent-primary" defaultValue="1" max="1" min="0" onChange={(event) => changeVolume(Number(event.currentTarget.value))} step="0.05" type="range" />
|
||||
</div>
|
||||
{hasError ? (
|
||||
<div className="flex min-w-0 flex-col gap-2 rounded-md border border-destructive bg-card p-3 text-sm text-destructive" role="alert">
|
||||
<p className="break-words">오디오를 재생할 수 없습니다. 페이지 새로고침 후 다시 시도하세요.</p>
|
||||
|
||||
Reference in New Issue
Block a user