test(voices): add useAudioPlayer hook unit test
AI Code Review / AI Code Review (pull_request) Failing after 0s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 6s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Successful in 35s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 36s

This commit is contained in:
SaaS Frontend
2026-07-25 14:25:39 +08:00
parent 137480d0c6
commit efbfeb91b2
+232
View File
@@ -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")
})
})