From efbfeb91b2f866b35dd251e2c6f64c7aa17902fa Mon Sep 17 00:00:00 2001 From: SaaS Frontend Date: Sat, 25 Jul 2026 14:25:39 +0800 Subject: [PATCH] test(voices): add useAudioPlayer hook unit test --- .../pages/voices/hooks/useAudioPlayer.test.ts | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100755 apps/web/src/test/pages/voices/hooks/useAudioPlayer.test.ts diff --git a/apps/web/src/test/pages/voices/hooks/useAudioPlayer.test.ts b/apps/web/src/test/pages/voices/hooks/useAudioPlayer.test.ts new file mode 100755 index 000000000..da94e45ce --- /dev/null +++ b/apps/web/src/test/pages/voices/hooks/useAudioPlayer.test.ts @@ -0,0 +1,232 @@ +/** + * useAudioPlayer hook 测试 — VoiceLibrary 版本 + * + * 该 Hook 使用 setInterval 模拟音频播放进度,纯逻辑可测。 + * 参考 voice-materials/hooks/useAudioPlayer.test.ts 的测试结构。 + */ +import { describe, it, expect, beforeEach, vi, afterEach } from "vitest" +import { renderHook, act } from "@testing-library/react" +import { useAudioPlayer } from "@/pages/voices/hooks/useAudioPlayer" + +describe("useAudioPlayer (voices)", () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + vi.clearAllMocks() + }) + + it("应该使用初始状态初始化", () => { + const { result } = renderHook(() => useAudioPlayer()) + + expect(result.current.playingId).toBeNull() + expect(result.current.currentTime).toBe(0) + }) + + it("handlePlay 应该开始播放指定音色", () => { + const { result } = renderHook(() => useAudioPlayer()) + + act(() => { + result.current.handlePlay("voice-1", 10) + }) + + expect(result.current.playingId).toBe("voice-1") + expect(result.current.currentTime).toBe(0) + }) + + it("handlePlay 对同一个音色不应重复启动播放", () => { + const { result } = renderHook(() => useAudioPlayer()) + + act(() => { + result.current.handlePlay("voice-1", 10) + }) + + const initialTime = result.current.currentTime + + // 推进一些时间让进度走动 + act(() => { + vi.advanceTimersByTime(200) + }) + + const timeAfterAdvance = result.current.currentTime + expect(timeAfterAdvance).toBeGreaterThan(initialTime) + + // 对同一个音色再次调用 handlePlay 不应重置 + act(() => { + result.current.handlePlay("voice-1", 10) + }) + + expect(result.current.playingId).toBe("voice-1") + expect(result.current.currentTime).toBe(timeAfterAdvance) + }) + + it("handlePlay 切换音色时应停止上一个并从头开始", () => { + const { result } = renderHook(() => useAudioPlayer()) + + act(() => { + result.current.handlePlay("voice-1", 10) + }) + + act(() => { + vi.advanceTimersByTime(500) + }) + + expect(result.current.playingId).toBe("voice-1") + expect(result.current.currentTime).toBeGreaterThan(0) + + act(() => { + result.current.handlePlay("voice-2", 15) + }) + + expect(result.current.playingId).toBe("voice-2") + expect(result.current.currentTime).toBe(0) + }) + + it("播放进度应该随时间递增", () => { + const { result } = renderHook(() => useAudioPlayer()) + + act(() => { + result.current.handlePlay("voice-1", 10) + }) + + // 每 100ms 增加 0.1 + act(() => { + vi.advanceTimersByTime(300) + }) + + expect(result.current.currentTime).toBeCloseTo(0.3, 1) + expect(result.current.playingId).toBe("voice-1") + }) + + it("播放到结尾应自动停止并重置", () => { + const { result } = renderHook(() => useAudioPlayer()) + + act(() => { + result.current.handlePlay("voice-1", 0.5) // 0.5 秒的短音频 + }) + + act(() => { + vi.advanceTimersByTime(600) // 超过 0.5 秒 + }) + + expect(result.current.playingId).toBeNull() + expect(result.current.currentTime).toBe(0) + }) + + it("handlePause 应该暂停播放", () => { + const { result } = renderHook(() => useAudioPlayer()) + + act(() => { + result.current.handlePlay("voice-1", 10) + }) + + act(() => { + vi.advanceTimersByTime(200) + }) + + const timeBeforePause = result.current.currentTime + + act(() => { + result.current.handlePause() + }) + + expect(result.current.playingId).toBeNull() + + // 暂停后时间不应再变化 + act(() => { + vi.advanceTimersByTime(500) + }) + + expect(result.current.currentTime).toBe(timeBeforePause) + }) + + it("handleSeek 应该跳转到指定时间", () => { + const { result } = renderHook(() => useAudioPlayer()) + + act(() => { + result.current.handlePlay("voice-1", 10) + }) + + act(() => { + result.current.handleSeek("voice-1", 5, 10) + }) + + expect(result.current.currentTime).toBe(5) + }) + + it("handleSeek 对不同音色应该开始播放该音色", () => { + const { result } = renderHook(() => useAudioPlayer()) + + act(() => { + result.current.handlePlay("voice-1", 10) + }) + + act(() => { + result.current.handleSeek("voice-2", 3, 15) + }) + + expect(result.current.playingId).toBe("voice-2") + expect(result.current.currentTime).toBe(3) + }) + + it("handleTogglePlay 应该在播放和暂停之间切换", () => { + const { result } = renderHook(() => useAudioPlayer()) + + // 初始为暂停,调用应开始播放 + act(() => { + result.current.handleTogglePlay("voice-1", 10) + }) + + expect(result.current.playingId).toBe("voice-1") + + // 再次调用应暂停 + act(() => { + result.current.handleTogglePlay("voice-1", 10) + }) + + expect(result.current.playingId).toBeNull() + }) + + it("stopPlayback 应该重置所有播放状态", () => { + const { result } = renderHook(() => useAudioPlayer()) + + act(() => { + result.current.handlePlay("voice-1", 10) + }) + + act(() => { + vi.advanceTimersByTime(300) + }) + + expect(result.current.playingId).toBe("voice-1") + expect(result.current.currentTime).toBeGreaterThan(0) + + act(() => { + result.current.stopPlayback() + }) + + expect(result.current.playingId).toBeNull() + expect(result.current.currentTime).toBe(0) + + // 停止后定时器不应再触发 + const timeAfterStop = result.current.currentTime + act(() => { + vi.advanceTimersByTime(500) + }) + expect(result.current.currentTime).toBe(timeAfterStop) + }) + + it("返回值应该包含所有必要的方法和状态", () => { + const { result } = renderHook(() => useAudioPlayer()) + + expect(typeof result.current.handlePlay).toBe("function") + expect(typeof result.current.handlePause).toBe("function") + expect(typeof result.current.handleSeek).toBe("function") + expect(typeof result.current.handleTogglePlay).toBe("function") + expect(typeof result.current.stopPlayback).toBe("function") + expect(typeof result.current.playingId).toBe("object") // string | null + expect(typeof result.current.currentTime).toBe("number") + }) +})