基于项目需求文档(PRD.md, FeatureSummary.md, DevelopmentPlan.md, UIDesign.md, User_Role_Interfaces.md)编写的 TDD 测试用例。 后端测试 (Python/pytest): - 单元测试: rule_engine, brief_parser, timestamp_alignment, video_auditor, validators - 集成测试: API Brief, Video, Review 端点 - AI 模块测试: ASR, OCR, Logo 检测服务 - 全局 fixtures 和 pytest 配置 前端测试 (TypeScript/Vitest): - 工具函数测试: utils.test.ts - 组件测试: Button, VideoPlayer, ViolationList - Hooks 测试: useVideoAudit, useVideoPlayer, useAppeal - MSW mock handlers 配置 E2E 测试 (Playwright): - 认证流程测试 - 视频上传流程测试 - 视频审核流程测试 - 申诉流程测试 所有测试当前使用 pytest.skip() / it.skip() 作为占位符, 遵循 TDD 红灯阶段 - 等待实现代码后运行。 验收标准覆盖: - ASR WER ≤ 10% - OCR 准确率 ≥ 95% - Logo F1 ≥ 0.85 - 时间戳误差 ≤ 0.5s - 频次统计准确率 ≥ 95% Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
205 lines
6.3 KiB
TypeScript
205 lines
6.3 KiB
TypeScript
/**
|
|
* VideoPlayer 组件单元测试
|
|
*
|
|
* TDD 测试用例 - 测试视频播放器组件
|
|
*
|
|
* UI 规范参考:UIDesign.md
|
|
*/
|
|
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
// import { VideoPlayer } from './VideoPlayer'
|
|
|
|
describe('VideoPlayer', () => {
|
|
const mockVideoSrc = 'https://example.com/video.mp4'
|
|
const mockViolations = [
|
|
{
|
|
id: 'vio_001',
|
|
timestamp_start: 5.0,
|
|
timestamp_end: 5.5,
|
|
type: 'prohibited_word',
|
|
content: '最好的',
|
|
severity: 'high',
|
|
},
|
|
{
|
|
id: 'vio_002',
|
|
timestamp_start: 10.0,
|
|
timestamp_end: 15.0,
|
|
type: 'competitor_logo',
|
|
content: 'CompetitorBrand',
|
|
severity: 'medium',
|
|
},
|
|
]
|
|
|
|
it.skip('should render video element', () => {
|
|
// render(<VideoPlayer src={mockVideoSrc} />)
|
|
// expect(screen.getByTestId('video-element')).toBeInTheDocument()
|
|
})
|
|
|
|
it.skip('should show loading state initially', () => {
|
|
// render(<VideoPlayer src={mockVideoSrc} />)
|
|
// expect(screen.getByTestId('loading-spinner')).toBeInTheDocument()
|
|
})
|
|
|
|
describe('playback controls', () => {
|
|
it.skip('should toggle play/pause on click', () => {
|
|
// render(<VideoPlayer src={mockVideoSrc} />)
|
|
// const playButton = screen.getByRole('button', { name: /play/i })
|
|
//
|
|
// fireEvent.click(playButton)
|
|
// expect(screen.getByRole('button', { name: /pause/i })).toBeInTheDocument()
|
|
//
|
|
// fireEvent.click(screen.getByRole('button', { name: /pause/i }))
|
|
// expect(screen.getByRole('button', { name: /play/i })).toBeInTheDocument()
|
|
})
|
|
|
|
it.skip('should show current time and duration', () => {
|
|
// render(<VideoPlayer src={mockVideoSrc} duration={60000} />)
|
|
// expect(screen.getByText('00:00 / 01:00')).toBeInTheDocument()
|
|
})
|
|
|
|
it.skip('should update time on seek', () => {
|
|
// render(<VideoPlayer src={mockVideoSrc} duration={60000} />)
|
|
// const seekBar = screen.getByRole('slider', { name: /seek/i })
|
|
//
|
|
// fireEvent.change(seekBar, { target: { value: 30000 } })
|
|
// expect(screen.getByText('00:30 / 01:00')).toBeInTheDocument()
|
|
})
|
|
|
|
it.skip('should toggle mute', () => {
|
|
// render(<VideoPlayer src={mockVideoSrc} />)
|
|
// const muteButton = screen.getByRole('button', { name: /mute/i })
|
|
//
|
|
// fireEvent.click(muteButton)
|
|
// expect(screen.getByRole('button', { name: /unmute/i })).toBeInTheDocument()
|
|
})
|
|
|
|
it.skip('should toggle fullscreen', () => {
|
|
// render(<VideoPlayer src={mockVideoSrc} />)
|
|
// const fullscreenButton = screen.getByRole('button', { name: /fullscreen/i })
|
|
//
|
|
// fireEvent.click(fullscreenButton)
|
|
// // 验证全屏 API 被调用
|
|
})
|
|
})
|
|
|
|
describe('violation markers', () => {
|
|
it.skip('should display violation markers on timeline', () => {
|
|
// render(
|
|
// <VideoPlayer
|
|
// src={mockVideoSrc}
|
|
// duration={60000}
|
|
// violations={mockViolations}
|
|
// />
|
|
// )
|
|
//
|
|
// const markers = screen.getAllByTestId('violation-marker')
|
|
// expect(markers).toHaveLength(2)
|
|
})
|
|
|
|
it.skip('should show violation tooltip on marker hover', async () => {
|
|
// render(
|
|
// <VideoPlayer
|
|
// src={mockVideoSrc}
|
|
// duration={60000}
|
|
// violations={mockViolations}
|
|
// />
|
|
// )
|
|
//
|
|
// const marker = screen.getAllByTestId('violation-marker')[0]
|
|
// fireEvent.mouseEnter(marker)
|
|
//
|
|
// await waitFor(() => {
|
|
// expect(screen.getByText('最好的')).toBeInTheDocument()
|
|
// })
|
|
})
|
|
|
|
it.skip('should seek to violation time on marker click', () => {
|
|
// const onSeek = vi.fn()
|
|
// render(
|
|
// <VideoPlayer
|
|
// src={mockVideoSrc}
|
|
// duration={60000}
|
|
// violations={mockViolations}
|
|
// onSeek={onSeek}
|
|
// />
|
|
// )
|
|
//
|
|
// const marker = screen.getAllByTestId('violation-marker')[0]
|
|
// fireEvent.click(marker)
|
|
//
|
|
// expect(onSeek).toHaveBeenCalledWith(5000) // 5.0 seconds in ms
|
|
})
|
|
|
|
it.skip('should highlight marker by severity color', () => {
|
|
// render(
|
|
// <VideoPlayer
|
|
// src={mockVideoSrc}
|
|
// duration={60000}
|
|
// violations={mockViolations}
|
|
// />
|
|
// )
|
|
//
|
|
// const markers = screen.getAllByTestId('violation-marker')
|
|
// expect(markers[0]).toHaveClass('bg-red-500') // high severity
|
|
// expect(markers[1]).toHaveClass('bg-orange-500') // medium severity
|
|
})
|
|
})
|
|
|
|
describe('keyboard navigation', () => {
|
|
it.skip('should play/pause with space key', () => {
|
|
// render(<VideoPlayer src={mockVideoSrc} />)
|
|
// const player = screen.getByTestId('video-player')
|
|
//
|
|
// fireEvent.keyDown(player, { key: ' ' })
|
|
// // 验证播放状态切换
|
|
})
|
|
|
|
it.skip('should seek forward with arrow right', () => {
|
|
// render(<VideoPlayer src={mockVideoSrc} />)
|
|
// const player = screen.getByTestId('video-player')
|
|
//
|
|
// fireEvent.keyDown(player, { key: 'ArrowRight' })
|
|
// // 验证前进 5 秒
|
|
})
|
|
|
|
it.skip('should seek backward with arrow left', () => {
|
|
// render(<VideoPlayer src={mockVideoSrc} />)
|
|
// const player = screen.getByTestId('video-player')
|
|
//
|
|
// fireEvent.keyDown(player, { key: 'ArrowLeft' })
|
|
// // 验证后退 5 秒
|
|
})
|
|
})
|
|
|
|
describe('playback rate', () => {
|
|
it.skip('should allow changing playback speed', () => {
|
|
// render(<VideoPlayer src={mockVideoSrc} />)
|
|
// const speedButton = screen.getByRole('button', { name: /speed/i })
|
|
//
|
|
// fireEvent.click(speedButton)
|
|
// fireEvent.click(screen.getByText('1.5x'))
|
|
//
|
|
// // 验证播放速度已更改
|
|
})
|
|
})
|
|
|
|
describe('error handling', () => {
|
|
it.skip('should show error message on load failure', async () => {
|
|
// render(<VideoPlayer src="invalid-url" />)
|
|
//
|
|
// await waitFor(() => {
|
|
// expect(screen.getByText(/加载失败/)).toBeInTheDocument()
|
|
// })
|
|
})
|
|
|
|
it.skip('should provide retry option on error', async () => {
|
|
// render(<VideoPlayer src="invalid-url" />)
|
|
//
|
|
// await waitFor(() => {
|
|
// expect(screen.getByRole('button', { name: /重试/ })).toBeInTheDocument()
|
|
// })
|
|
})
|
|
})
|
|
})
|