feat: 添加全面的 TDD 测试套件框架

基于项目需求文档(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>
This commit is contained in:
Your Name
2026-02-02 17:22:24 +08:00
co-authored by Claude Opus 4.5
parent 18fe22ce8a
commit 040aada160
26 changed files with 6185 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
import { defineConfig, devices } from '@playwright/test'
/**
* Playwright E2E 测试配置
*
* 用于端到端测试,覆盖关键用户流程
*/
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html', { open: 'never' }],
['json', { outputFile: 'test-results/results.json' }],
],
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// 移动端测试
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
// 启动开发服务器
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
})
+210
View File
@@ -0,0 +1,210 @@
/**
* 申诉流程 E2E 测试
*
* TDD 测试用例 - 测试达人申诉和审核员处理申诉的流程
*
* 用户流程参考:User_Role_Interfaces.md
*/
import { test, expect } from '@playwright/test'
test.describe('Creator Appeal Flow', () => {
test.beforeEach(async ({ page }) => {
// 以达人身份登录
// await page.goto('/login')
// await page.getByPlaceholder('邮箱').fill('creator@example.com')
// await page.getByPlaceholder('密码').fill('password123')
// await page.getByRole('button', { name: '登录' }).click()
// await page.waitForURL('/dashboard')
})
test.skip('should display appeal tokens', async ({ page }) => {
// await page.goto('/dashboard')
//
// // 验证显示申诉令牌数量
// await expect(page.getByTestId('appeal-tokens')).toBeVisible()
// await expect(page.getByText('剩余申诉次数:3')).toBeVisible()
})
test.skip('should open appeal form from rejected video', async ({ page }) => {
// await page.goto('/videos/video_rejected')
//
// // 验证显示驳回状态
// await expect(page.getByText('已驳回')).toBeVisible()
//
// // 验证显示申诉按钮
// await expect(page.getByRole('button', { name: '申诉' })).toBeVisible()
//
// // 点击申诉
// await page.getByRole('button', { name: '申诉' }).click()
//
// // 验证申诉表单
// await expect(page.getByTestId('appeal-form')).toBeVisible()
})
test.skip('should select violations to appeal', async ({ page }) => {
// await page.goto('/videos/video_rejected')
// await page.getByRole('button', { name: '申诉' }).click()
//
// // 显示违规列表
// const violationList = page.getByTestId('appeal-violation-list')
// await expect(violationList).toBeVisible()
//
// // 选择要申诉的违规项
// await violationList.getByRole('checkbox').first().click()
//
// // 验证已选择
// await expect(page.getByText('已选择 1 项')).toBeVisible()
})
test.skip('should require appeal reason >= 10 characters', async ({ page }) => {
// await page.goto('/videos/video_rejected')
// await page.getByRole('button', { name: '申诉' }).click()
//
// // 选择违规项
// await page.getByTestId('appeal-violation-list').getByRole('checkbox').first().click()
//
// // 输入过短的理由
// await page.getByPlaceholder('请输入申诉理由').fill('太短了')
//
// // 尝试提交
// await page.getByRole('button', { name: '提交申诉' }).click()
//
// // 验证错误提示
// await expect(page.getByText('申诉理由至少 10 个字')).toBeVisible()
})
test.skip('should submit appeal successfully', async ({ page }) => {
// await page.goto('/videos/video_rejected')
// await page.getByRole('button', { name: '申诉' }).click()
//
// // 选择违规项
// await page.getByTestId('appeal-violation-list').getByRole('checkbox').first().click()
//
// // 输入申诉理由
// await page.getByPlaceholder('请输入申诉理由').fill('这个词语在此语境下是正常使用,表达的是个人主观感受,不应被判定为违规广告语')
//
// // 提交申诉
// await page.getByRole('button', { name: '提交申诉' }).click()
//
// // 验证成功
// await expect(page.getByText('申诉已提交')).toBeVisible()
// await expect(page.getByText('申诉中')).toBeVisible()
})
test.skip('should deduct appeal token on submit', async ({ page }) => {
// // 获取当前令牌数
// await page.goto('/dashboard')
// const initialTokens = await page.getByTestId('appeal-tokens').textContent()
//
// // 提交申诉
// await page.goto('/videos/video_rejected')
// await page.getByRole('button', { name: '申诉' }).click()
// await page.getByTestId('appeal-violation-list').getByRole('checkbox').first().click()
// await page.getByPlaceholder('请输入申诉理由').fill('这个词语在此语境下是正常使用,不应被判定为违规')
// await page.getByRole('button', { name: '提交申诉' }).click()
//
// // 验证令牌已扣除
// await page.goto('/dashboard')
// const newTokens = await page.getByTestId('appeal-tokens').textContent()
// expect(parseInt(newTokens!)).toBe(parseInt(initialTokens!) - 1)
})
test.skip('should show error when no tokens available', async ({ page }) => {
// // 假设用户无令牌
// await page.goto('/videos/video_rejected')
//
// // 点击申诉按钮
// await page.getByRole('button', { name: '申诉' }).click()
//
// // 验证提示无令牌
// await expect(page.getByText('申诉次数已用完')).toBeVisible()
// await expect(page.getByText('联系管理员')).toBeVisible()
})
})
test.describe('Reviewer Process Appeal', () => {
test.beforeEach(async ({ page }) => {
// 以 Agency 审核员身份登录
// await page.goto('/login')
// await page.getByPlaceholder('邮箱').fill('agency@example.com')
// await page.getByPlaceholder('密码').fill('password123')
// await page.getByRole('button', { name: '登录' }).click()
// await page.waitForURL('/dashboard')
})
test.skip('should display appeal list', async ({ page }) => {
// await page.goto('/appeals')
//
// await expect(page.getByRole('heading', { name: '申诉处理' })).toBeVisible()
// await expect(page.getByTestId('appeal-list')).toBeVisible()
})
test.skip('should show appeal details', async ({ page }) => {
// await page.goto('/appeals/appeal_001')
//
// // 验证显示申诉信息
// await expect(page.getByTestId('appeal-reason')).toBeVisible()
// await expect(page.getByTestId('original-violation')).toBeVisible()
// await expect(page.getByTestId('video-preview')).toBeVisible()
})
test.skip('should approve appeal', async ({ page }) => {
// await page.goto('/appeals/appeal_001')
//
// // 点击通过申诉
// await page.getByRole('button', { name: '申诉成立' }).click()
//
// // 填写处理意见
// await page.getByPlaceholder('处理意见').fill('申诉理由成立')
//
// // 确认
// await page.getByRole('button', { name: '确认' }).click()
//
// // 验证成功
// await expect(page.getByText('申诉已处理')).toBeVisible()
})
test.skip('should reject appeal', async ({ page }) => {
// await page.goto('/appeals/appeal_001')
//
// // 点击驳回申诉
// await page.getByRole('button', { name: '申诉不成立' }).click()
//
// // 填写处理意见
// await page.getByPlaceholder('处理意见').fill('违规判定正确')
//
// // 确认
// await page.getByRole('button', { name: '确认' }).click()
//
// // 验证成功
// await expect(page.getByText('申诉已处理')).toBeVisible()
})
test.skip('should restore token on appeal approval', async ({ page }) => {
// // 审批通过申诉后,达人的令牌应该返还
// // 这个测试需要跨用户验证,可能需要特殊处理
})
})
test.describe('Appeal Status Tracking', () => {
test.skip('should show appeal status in video list', async ({ page }) => {
// await page.goto('/videos')
//
// // 找到正在申诉的视频
// const appealingVideo = page.getByTestId('video-item').filter({ hasText: '申诉中' })
// await expect(appealingVideo).toBeVisible()
//
// // 验证显示申诉状态徽章
// await expect(appealingVideo.getByTestId('appeal-badge')).toBeVisible()
})
test.skip('should notify on appeal result', async ({ page }) => {
// await page.goto('/dashboard')
//
// // 验证通知中心显示申诉结果
// await page.getByRole('button', { name: '通知' }).click()
//
// await expect(page.getByText('申诉结果')).toBeVisible()
})
})
+119
View File
@@ -0,0 +1,119 @@
/**
* 认证流程 E2E 测试
*
* TDD 测试用例 - 测试登录、登出、权限验证
*/
import { test, expect } from '@playwright/test'
test.describe('Authentication', () => {
test.skip('should display login page', async ({ page }) => {
// await page.goto('/login')
//
// await expect(page.getByRole('heading', { name: '登录' })).toBeVisible()
// await expect(page.getByPlaceholder('邮箱')).toBeVisible()
// await expect(page.getByPlaceholder('密码')).toBeVisible()
// await expect(page.getByRole('button', { name: '登录' })).toBeVisible()
})
test.skip('should login with valid credentials', async ({ page }) => {
// await page.goto('/login')
//
// await page.getByPlaceholder('邮箱').fill('test@example.com')
// await page.getByPlaceholder('密码').fill('password123')
// await page.getByRole('button', { name: '登录' }).click()
//
// // 登录成功后跳转到首页
// await expect(page).toHaveURL('/dashboard')
// await expect(page.getByText('欢迎回来')).toBeVisible()
})
test.skip('should show error for invalid credentials', async ({ page }) => {
// await page.goto('/login')
//
// await page.getByPlaceholder('邮箱').fill('wrong@example.com')
// await page.getByPlaceholder('密码').fill('wrongpassword')
// await page.getByRole('button', { name: '登录' }).click()
//
// await expect(page.getByText('邮箱或密码错误')).toBeVisible()
// await expect(page).toHaveURL('/login')
})
test.skip('should logout successfully', async ({ page }) => {
// // 先登录
// await page.goto('/login')
// await page.getByPlaceholder('邮箱').fill('test@example.com')
// await page.getByPlaceholder('密码').fill('password123')
// await page.getByRole('button', { name: '登录' }).click()
//
// // 点击登出
// await page.getByRole('button', { name: /用户菜单/ }).click()
// await page.getByRole('menuitem', { name: '退出登录' }).click()
//
// // 验证跳转到登录页
// await expect(page).toHaveURL('/login')
})
test.skip('should redirect unauthenticated users to login', async ({ page }) => {
// await page.goto('/dashboard')
//
// // 未登录用户应被重定向到登录页
// await expect(page).toHaveURL('/login?redirect=/dashboard')
})
test.skip('should redirect to original page after login', async ({ page }) => {
// // 尝试访问受保护页面
// await page.goto('/videos/video_001')
//
// // 被重定向到登录页
// await expect(page).toHaveURL(/\/login.*redirect/)
//
// // 登录
// await page.getByPlaceholder('邮箱').fill('test@example.com')
// await page.getByPlaceholder('密码').fill('password123')
// await page.getByRole('button', { name: '登录' }).click()
//
// // 登录后应跳转到原来的页面
// await expect(page).toHaveURL('/videos/video_001')
})
})
test.describe('Role-based Access', () => {
test.skip('creator should see creator-specific menu', async ({ page }) => {
// // 以达人身份登录
// await page.goto('/login')
// await page.getByPlaceholder('邮箱').fill('creator@example.com')
// await page.getByPlaceholder('密码').fill('password123')
// await page.getByRole('button', { name: '登录' }).click()
//
// // 验证达人菜单
// await expect(page.getByRole('link', { name: '我的视频' })).toBeVisible()
// await expect(page.getByRole('link', { name: '提交视频' })).toBeVisible()
// // 不应看到管理功能
// await expect(page.getByRole('link', { name: '用户管理' })).not.toBeVisible()
})
test.skip('agency should see review options', async ({ page }) => {
// // 以 Agency 身份登录
// await page.goto('/login')
// await page.getByPlaceholder('邮箱').fill('agency@example.com')
// await page.getByPlaceholder('密码').fill('password123')
// await page.getByRole('button', { name: '登录' }).click()
//
// // 验证 Agency 菜单
// await expect(page.getByRole('link', { name: '待审核' })).toBeVisible()
// await expect(page.getByRole('link', { name: '任务管理' })).toBeVisible()
})
test.skip('admin should see all menu items', async ({ page }) => {
// // 以管理员身份登录
// await page.goto('/login')
// await page.getByPlaceholder('邮箱').fill('admin@example.com')
// await page.getByPlaceholder('密码').fill('password123')
// await page.getByRole('button', { name: '登录' }).click()
//
// // 验证管理员菜单
// await expect(page.getByRole('link', { name: '用户管理' })).toBeVisible()
// await expect(page.getByRole('link', { name: '系统设置' })).toBeVisible()
})
})
+217
View File
@@ -0,0 +1,217 @@
/**
* 视频审核流程 E2E 测试
*
* TDD 测试用例 - 测试完整的视频审核用户流程
*
* 用户流程参考:User_Role_Interfaces.md
*/
import { test, expect } from '@playwright/test'
test.describe('Video Review Flow', () => {
test.beforeEach(async ({ page }) => {
// 以 Agency 审核员身份登录
// await page.goto('/login')
// await page.getByPlaceholder('邮箱').fill('agency@example.com')
// await page.getByPlaceholder('密码').fill('password123')
// await page.getByRole('button', { name: '登录' }).click()
// await page.waitForURL('/dashboard')
})
test.skip('should display pending review list', async ({ page }) => {
// await page.goto('/reviews/pending')
//
// // 验证列表显示
// await expect(page.getByRole('heading', { name: '待审核视频' })).toBeVisible()
// await expect(page.getByTestId('video-list')).toBeVisible()
//
// // 验证列表项
// const videos = page.getByTestId('video-item')
// await expect(videos).toHaveCount.greaterThan(0)
})
test.skip('should open video review page', async ({ page }) => {
// await page.goto('/reviews/pending')
//
// // 点击第一个视频
// await page.getByTestId('video-item').first().click()
//
// // 验证审核页面
// await expect(page.getByTestId('video-player')).toBeVisible()
// await expect(page.getByTestId('violation-list')).toBeVisible()
// await expect(page.getByTestId('brief-compliance')).toBeVisible()
})
test.skip('should play video and seek to violation', async ({ page }) => {
// await page.goto('/reviews/video_001')
//
// // 找到违规项
// const violation = page.getByTestId('violation-item').first()
// await expect(violation).toBeVisible()
//
// // 点击时间戳跳转
// await violation.getByTestId('timestamp-link').click()
//
// // 验证视频跳转到对应时间
// const currentTime = page.getByTestId('current-time')
// await expect(currentTime).toContainText('00:05')
})
test.skip('should show violation evidence screenshot', async ({ page }) => {
// await page.goto('/reviews/video_001')
//
// // 找到 Logo 违规项
// const logoViolation = page.getByText('竞品 Logo').locator('..')
//
// // 点击查看证据
// await logoViolation.getByRole('button', { name: '查看证据' }).click()
//
// // 验证截图显示
// await expect(page.getByTestId('evidence-modal')).toBeVisible()
// await expect(page.getByRole('img', { name: '违规截图' })).toBeVisible()
})
test.skip('should pass video review', async ({ page }) => {
// await page.goto('/reviews/video_001')
//
// // 点击通过按钮
// await page.getByRole('button', { name: '通过' }).click()
//
// // 填写评语(可选)
// await page.getByPlaceholder('审核评语').fill('内容符合要求')
//
// // 确认提交
// await page.getByRole('button', { name: '确认通过' }).click()
//
// // 验证成功提示
// await expect(page.getByText('审核完成')).toBeVisible()
//
// // 验证跳转到下一个待审核视频或列表
// await expect(page).toHaveURL(/\/reviews/)
})
test.skip('should reject video with selected violations', async ({ page }) => {
// await page.goto('/reviews/video_001')
//
// // 选择违规项
// await page.getByTestId('violation-checkbox').first().click()
// await page.getByTestId('violation-checkbox').nth(1).click()
//
// // 点击驳回按钮
// await page.getByRole('button', { name: '驳回' }).click()
//
// // 验证显示已选违规项
// const modal = page.getByTestId('reject-modal')
// await expect(modal).toBeVisible()
// await expect(modal.getByText('已选择 2 项违规')).toBeVisible()
//
// // 填写评语
// await modal.getByPlaceholder('审核评语').fill('存在违规内容,请修改')
//
// // 确认驳回
// await modal.getByRole('button', { name: '确认驳回' }).click()
//
// // 验证成功提示
// await expect(page.getByText('已驳回')).toBeVisible()
})
test.skip('should force pass with reason', async ({ page }) => {
// await page.goto('/reviews/video_001')
//
// // 点击强制通过
// await page.getByRole('button', { name: '强制通过' }).click()
//
// // 验证需要填写原因
// const modal = page.getByTestId('force-pass-modal')
// await expect(modal).toBeVisible()
//
// // 尝试不填原因提交
// await modal.getByRole('button', { name: '确认' }).click()
// await expect(modal.getByText('请填写强制通过原因')).toBeVisible()
//
// // 填写原因
// await modal.getByPlaceholder('请填写原因').fill('达人玩的新梗,品牌方认可')
// await modal.getByRole('button', { name: '确认' }).click()
//
// // 验证成功
// await expect(page.getByText('强制通过成功')).toBeVisible()
})
test.skip('should add manual violation', async ({ page }) => {
// await page.goto('/reviews/video_001')
//
// // 点击添加违规
// await page.getByRole('button', { name: '添加违规项' }).click()
//
// // 填写违规信息
// const modal = page.getByTestId('add-violation-modal')
// await modal.getByLabel('违规类型').selectOption('other')
// await modal.getByPlaceholder('违规内容').fill('发现额外问题')
// await modal.getByLabel('开始时间').fill('00:10')
// await modal.getByLabel('结束时间').fill('00:15')
// await modal.getByLabel('严重程度').selectOption('medium')
//
// // 提交
// await modal.getByRole('button', { name: '添加' }).click()
//
// // 验证违规项已添加
// await expect(page.getByText('发现额外问题')).toBeVisible()
// await expect(page.getByTestId('violation-item').filter({ hasText: '手动添加' })).toBeVisible()
})
test.skip('should delete AI violation', async ({ page }) => {
// await page.goto('/reviews/video_001')
//
// // 找到 AI 检测的违规项
// const aiViolation = page.getByTestId('violation-item').filter({ hasText: 'AI 检测' }).first()
//
// // 点击删除
// await aiViolation.getByRole('button', { name: '删除' }).click()
//
// // 确认删除
// const confirmModal = page.getByTestId('confirm-modal')
// await confirmModal.getByPlaceholder('删除原因').fill('误检')
// await confirmModal.getByRole('button', { name: '确认删除' }).click()
//
// // 验证违规项已删除
// await expect(aiViolation).not.toBeVisible()
})
})
test.describe('Brief Compliance Check', () => {
test.skip('should display brief compliance status', async ({ page }) => {
// await page.goto('/reviews/video_001')
//
// // 验证 Brief 合规面板
// const compliancePanel = page.getByTestId('brief-compliance')
// await expect(compliancePanel).toBeVisible()
//
// // 验证卖点覆盖
// await expect(compliancePanel.getByText('卖点覆盖')).toBeVisible()
// await expect(compliancePanel.getByText('2/3')).toBeVisible()
//
// // 验证时长要求
// await expect(compliancePanel.getByText('产品同框')).toBeVisible()
})
test.skip('should show uncovered selling points', async ({ page }) => {
// await page.goto('/reviews/video_001')
//
// // 点击查看详情
// await page.getByTestId('brief-compliance').getByRole('button', { name: '查看详情' }).click()
//
// // 验证显示未覆盖的卖点
// const detailModal = page.getByTestId('compliance-detail-modal')
// await expect(detailModal.getByText('未覆盖')).toBeVisible()
// await expect(detailModal.getByText('敏感肌适用')).toBeVisible()
})
test.skip('should highlight timing requirement failures', async ({ page }) => {
// await page.goto('/reviews/video_001')
//
// // 验证不合规的时长要求显示为红色
// const timingRequirement = page.getByTestId('timing-requirement').filter({ hasText: '品牌名提及' })
// await expect(timingRequirement).toHaveClass(/text-red/)
// await expect(timingRequirement.getByText('2/3')).toBeVisible() // 未达标
})
})
+201
View File
@@ -0,0 +1,201 @@
/**
* 视频上传流程 E2E 测试
*
* TDD 测试用例 - 测试达人上传视频的完整流程
*
* 用户流程参考:User_Role_Interfaces.md
*/
import { test, expect } from '@playwright/test'
import path from 'path'
test.describe('Video Upload Flow', () => {
test.beforeEach(async ({ page }) => {
// 以达人身份登录
// await page.goto('/login')
// await page.getByPlaceholder('邮箱').fill('creator@example.com')
// await page.getByPlaceholder('密码').fill('password123')
// await page.getByRole('button', { name: '登录' }).click()
// await page.waitForURL('/dashboard')
})
test.skip('should display upload page', async ({ page }) => {
// await page.goto('/videos/upload')
//
// await expect(page.getByRole('heading', { name: '上传视频' })).toBeVisible()
// await expect(page.getByTestId('upload-dropzone')).toBeVisible()
// await expect(page.getByText('支持 MP4、MOV 格式')).toBeVisible()
// await expect(page.getByText('最大 100MB')).toBeVisible()
})
test.skip('should select task before upload', async ({ page }) => {
// await page.goto('/videos/upload')
//
// // 验证需要先选择任务
// await expect(page.getByLabel('选择任务')).toBeVisible()
//
// // 选择任务
// await page.getByLabel('选择任务').click()
// await page.getByRole('option', { name: 'XX美妆产品推广' }).click()
//
// // 验证任务信息显示
// await expect(page.getByText('Brief 要求')).toBeVisible()
})
test.skip('should upload video file', async ({ page }) => {
// await page.goto('/videos/upload')
//
// // 选择任务
// await page.getByLabel('选择任务').click()
// await page.getByRole('option').first().click()
//
// // 上传文件
// const fileInput = page.getByTestId('file-input')
// await fileInput.setInputFiles(path.join(__dirname, '../fixtures/sample-video.mp4'))
//
// // 验证上传进度
// await expect(page.getByTestId('upload-progress')).toBeVisible()
// await expect(page.getByText(/上传中/)).toBeVisible()
//
// // 等待上传完成
// await expect(page.getByText('上传成功')).toBeVisible({ timeout: 60000 })
})
test.skip('should show validation error for unsupported format', async ({ page }) => {
// await page.goto('/videos/upload')
//
// // 选择任务
// await page.getByLabel('选择任务').click()
// await page.getByRole('option').first().click()
//
// // 上传不支持的格式
// const fileInput = page.getByTestId('file-input')
// await fileInput.setInputFiles(path.join(__dirname, '../fixtures/sample.avi'))
//
// // 验证错误提示
// await expect(page.getByText('不支持的文件格式')).toBeVisible()
// await expect(page.getByText('仅支持 MP4、MOV')).toBeVisible()
})
test.skip('should show error for oversized file', async ({ page }) => {
// await page.goto('/videos/upload')
//
// // 选择任务
// await page.getByLabel('选择任务').click()
// await page.getByRole('option').first().click()
//
// // 尝试上传超大文件(模拟)
// // 由于无法真正创建超大文件,这里验证前端校验逻辑
//
// // 假设通过 JavaScript 注入一个超大文件
// await page.evaluate(() => {
// const file = new File(['x'.repeat(101 * 1024 * 1024)], 'large.mp4', { type: 'video/mp4' })
// const event = new Event('change', { bubbles: true })
// const input = document.querySelector('[data-testid="file-input"]') as HTMLInputElement
// Object.defineProperty(input, 'files', { value: [file] })
// input.dispatchEvent(event)
// })
//
// await expect(page.getByText('文件大小超过限制')).toBeVisible()
// await expect(page.getByText('最大 100MB')).toBeVisible()
})
test.skip('should show processing status after upload', async ({ page }) => {
// await page.goto('/videos/upload')
//
// // 选择任务并上传
// await page.getByLabel('选择任务').click()
// await page.getByRole('option').first().click()
//
// const fileInput = page.getByTestId('file-input')
// await fileInput.setInputFiles(path.join(__dirname, '../fixtures/sample-video.mp4'))
//
// // 等待上传完成
// await expect(page.getByText('上传成功')).toBeVisible({ timeout: 60000 })
//
// // 验证显示处理状态
// await expect(page.getByText('AI 审核中')).toBeVisible()
// await expect(page.getByTestId('processing-progress')).toBeVisible()
})
test.skip('should navigate to video detail after processing', async ({ page }) => {
// // 假设视频已上传并处理完成
// await page.goto('/videos/video_new')
//
// // 验证显示审核结果
// await expect(page.getByTestId('audit-result')).toBeVisible()
// await expect(page.getByText('AI 检测完成')).toBeVisible()
})
})
test.describe('Drag and Drop Upload', () => {
test.skip('should highlight dropzone on drag over', async ({ page }) => {
// await page.goto('/videos/upload')
//
// // 模拟拖拽进入
// const dropzone = page.getByTestId('upload-dropzone')
//
// // 触发 dragenter 事件
// await dropzone.dispatchEvent('dragenter', {
// dataTransfer: { types: ['Files'] },
// })
//
// // 验证高亮状态
// await expect(dropzone).toHaveClass(/border-primary/)
})
test.skip('should remove highlight on drag leave', async ({ page }) => {
// await page.goto('/videos/upload')
//
// const dropzone = page.getByTestId('upload-dropzone')
//
// // 触发 dragenter
// await dropzone.dispatchEvent('dragenter', {
// dataTransfer: { types: ['Files'] },
// })
//
// // 触发 dragleave
// await dropzone.dispatchEvent('dragleave')
//
// // 验证高亮已移除
// await expect(dropzone).not.toHaveClass(/border-primary/)
})
})
test.describe('Resumable Upload', () => {
test.skip('should resume interrupted upload', async ({ page }) => {
// await page.goto('/videos/upload')
//
// // 选择任务
// await page.getByLabel('选择任务').click()
// await page.getByRole('option').first().click()
//
// // 开始上传
// const fileInput = page.getByTestId('file-input')
// await fileInput.setInputFiles(path.join(__dirname, '../fixtures/sample-video.mp4'))
//
// // 等待开始上传
// await expect(page.getByTestId('upload-progress')).toBeVisible()
//
// // 模拟中断(刷新页面)
// await page.reload()
//
// // 验证显示恢复上传选项
// await expect(page.getByText('检测到未完成的上传')).toBeVisible()
// await expect(page.getByRole('button', { name: '继续上传' })).toBeVisible()
//
// // 点击继续上传
// await page.getByRole('button', { name: '继续上传' }).click()
//
// // 验证从中断处继续
// await expect(page.getByTestId('upload-progress')).toBeVisible()
})
test.skip('should allow canceling pending upload', async ({ page }) => {
// await page.goto('/videos/upload')
//
// // 如果有未完成的上传
// // await page.getByRole('button', { name: '取消' }).click()
// // await expect(page.getByText('已取消上传')).toBeVisible()
})
})
@@ -0,0 +1,268 @@
/**
* ViolationList 组件单元测试
*
* TDD 测试用例 - 测试违规项列表组件
*
* UI 规范参考:UIDesign.md 审核界面
*/
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent, within } from '@testing-library/react'
// import { ViolationList } from './ViolationList'
describe('ViolationList', () => {
const mockViolations = [
{
id: 'vio_001',
type: 'prohibited_word',
content: '最好的',
timestamp_start: 5.0,
timestamp_end: 5.5,
severity: 'high',
source: 'ai',
context: '这是最好的产品',
},
{
id: 'vio_002',
type: 'competitor_logo',
content: 'CompetitorBrand',
timestamp_start: 10.0,
timestamp_end: 15.0,
severity: 'medium',
source: 'ai',
screenshot_url: 'https://example.com/screenshot.jpg',
},
{
id: 'vio_003',
type: 'brand_tone',
content: '表达过于生硬',
timestamp_start: 20.0,
timestamp_end: 25.0,
severity: 'low',
source: 'manual',
},
]
it.skip('should render all violations', () => {
// render(<ViolationList violations={mockViolations} />)
//
// expect(screen.getAllByTestId('violation-item')).toHaveLength(3)
})
it.skip('should display violation type label', () => {
// render(<ViolationList violations={mockViolations} />)
//
// expect(screen.getByText('禁用词')).toBeInTheDocument()
// expect(screen.getByText('竞品 Logo')).toBeInTheDocument()
// expect(screen.getByText('品牌调性')).toBeInTheDocument()
})
it.skip('should display violation content', () => {
// render(<ViolationList violations={mockViolations} />)
//
// expect(screen.getByText('最好的')).toBeInTheDocument()
// expect(screen.getByText('CompetitorBrand')).toBeInTheDocument()
})
it.skip('should display timestamp', () => {
// render(<ViolationList violations={mockViolations} />)
//
// expect(screen.getByText('00:05 - 00:05')).toBeInTheDocument()
// expect(screen.getByText('00:10 - 00:15')).toBeInTheDocument()
})
it.skip('should show severity badge with correct color', () => {
// render(<ViolationList violations={mockViolations} />)
//
// const items = screen.getAllByTestId('violation-item')
//
// expect(within(items[0]).getByTestId('severity-badge')).toHaveClass('bg-red-500')
// expect(within(items[1]).getByTestId('severity-badge')).toHaveClass('bg-orange-500')
// expect(within(items[2]).getByTestId('severity-badge')).toHaveClass('bg-yellow-500')
})
describe('selection', () => {
it.skip('should allow selecting violations', () => {
// const onSelectionChange = vi.fn()
// render(
// <ViolationList
// violations={mockViolations}
// selectable
// onSelectionChange={onSelectionChange}
// />
// )
//
// const checkbox = screen.getAllByRole('checkbox')[0]
// fireEvent.click(checkbox)
//
// expect(onSelectionChange).toHaveBeenCalledWith(['vio_001'])
})
it.skip('should support select all', () => {
// const onSelectionChange = vi.fn()
// render(
// <ViolationList
// violations={mockViolations}
// selectable
// onSelectionChange={onSelectionChange}
// />
// )
//
// const selectAllCheckbox = screen.getByRole('checkbox', { name: /全选/ })
// fireEvent.click(selectAllCheckbox)
//
// expect(onSelectionChange).toHaveBeenCalledWith(['vio_001', 'vio_002', 'vio_003'])
})
it.skip('should show indeterminate state when partially selected', () => {
// render(
// <ViolationList
// violations={mockViolations}
// selectable
// selectedIds={['vio_001']}
// />
// )
//
// const selectAllCheckbox = screen.getByRole('checkbox', { name: /全选/ })
// expect(selectAllCheckbox).toHaveAttribute('aria-checked', 'mixed')
})
})
describe('actions', () => {
it.skip('should call onSeek when timestamp is clicked', () => {
// const onSeek = vi.fn()
// render(
// <ViolationList
// violations={mockViolations}
// onSeek={onSeek}
// />
// )
//
// const timestampLink = screen.getByText('00:05 - 00:05')
// fireEvent.click(timestampLink)
//
// expect(onSeek).toHaveBeenCalledWith(5000)
})
it.skip('should show delete button for manual violations', () => {
// render(
// <ViolationList
// violations={mockViolations}
// editable
// />
// )
//
// const manualItem = screen.getAllByTestId('violation-item')[2]
// expect(within(manualItem).getByRole('button', { name: /删除/ })).toBeInTheDocument()
})
it.skip('should call onDelete when delete button is clicked', () => {
// const onDelete = vi.fn()
// render(
// <ViolationList
// violations={mockViolations}
// editable
// onDelete={onDelete}
// />
// )
//
// const manualItem = screen.getAllByTestId('violation-item')[2]
// const deleteButton = within(manualItem).getByRole('button', { name: /删除/ })
// fireEvent.click(deleteButton)
//
// expect(onDelete).toHaveBeenCalledWith('vio_003')
})
})
describe('filtering', () => {
it.skip('should filter by severity', () => {
// render(
// <ViolationList
// violations={mockViolations}
// filterBySeverity="high"
// />
// )
//
// expect(screen.getAllByTestId('violation-item')).toHaveLength(1)
// expect(screen.getByText('最好的')).toBeInTheDocument()
})
it.skip('should filter by type', () => {
// render(
// <ViolationList
// violations={mockViolations}
// filterByType="prohibited_word"
// />
// )
//
// expect(screen.getAllByTestId('violation-item')).toHaveLength(1)
})
it.skip('should filter by source (AI vs manual)', () => {
// render(
// <ViolationList
// violations={mockViolations}
// filterBySource="ai"
// />
// )
//
// expect(screen.getAllByTestId('violation-item')).toHaveLength(2)
})
})
describe('sorting', () => {
it.skip('should sort by timestamp ascending by default', () => {
// render(<ViolationList violations={mockViolations} />)
//
// const items = screen.getAllByTestId('violation-item')
// expect(within(items[0]).getByText('00:05')).toBeInTheDocument()
// expect(within(items[1]).getByText('00:10')).toBeInTheDocument()
// expect(within(items[2]).getByText('00:20')).toBeInTheDocument()
})
it.skip('should allow sorting by severity', () => {
// render(<ViolationList violations={mockViolations} sortBy="severity" />)
//
// const items = screen.getAllByTestId('violation-item')
// // high -> medium -> low
// expect(within(items[0]).getByText('最好的')).toBeInTheDocument()
})
})
describe('empty state', () => {
it.skip('should show empty state when no violations', () => {
// render(<ViolationList violations={[]} />)
//
// expect(screen.getByText('暂无违规项')).toBeInTheDocument()
})
it.skip('should show custom empty message', () => {
// render(
// <ViolationList
// violations={[]}
// emptyMessage="AI 未检测到任何问题"
// />
// )
//
// expect(screen.getByText('AI 未检测到任何问题')).toBeInTheDocument()
})
})
describe('evidence preview', () => {
it.skip('should show screenshot preview for logo violations', () => {
// render(<ViolationList violations={mockViolations} />)
//
// const logoItem = screen.getAllByTestId('violation-item')[1]
// expect(within(logoItem).getByRole('img')).toHaveAttribute(
// 'src',
// 'https://example.com/screenshot.jpg'
// )
})
it.skip('should show context for text violations', () => {
// render(<ViolationList violations={mockViolations} showContext />)
//
// expect(screen.getByText('这是最好的产品')).toBeInTheDocument()
})
})
})
+127
View File
@@ -0,0 +1,127 @@
/**
* Button 组件单元测试
*
* TDD 测试用例 - 测试按钮组件的各种状态和交互
*/
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
// import { Button } from './Button'
describe('Button', () => {
it.skip('should render button with text', () => {
// render(<Button>点击</Button>)
// expect(screen.getByRole('button')).toHaveTextContent('点击')
})
it.skip('should handle click events', () => {
// const handleClick = vi.fn()
// render(<Button onClick={handleClick}>点击</Button>)
//
// fireEvent.click(screen.getByRole('button'))
// expect(handleClick).toHaveBeenCalledTimes(1)
})
it.skip('should be disabled when disabled prop is true', () => {
// const handleClick = vi.fn()
// render(<Button disabled onClick={handleClick}>点击</Button>)
//
// const button = screen.getByRole('button')
// expect(button).toBeDisabled()
//
// fireEvent.click(button)
// expect(handleClick).not.toHaveBeenCalled()
})
it.skip('should show loading state', () => {
// render(<Button loading>提交</Button>)
//
// expect(screen.getByRole('button')).toBeDisabled()
// expect(screen.getByTestId('loading-spinner')).toBeInTheDocument()
})
describe('variants', () => {
it.skip('should render primary variant by default', () => {
// render(<Button>Primary</Button>)
// expect(screen.getByRole('button')).toHaveClass('bg-primary')
})
it.skip('should render secondary variant', () => {
// render(<Button variant="secondary">Secondary</Button>)
// expect(screen.getByRole('button')).toHaveClass('bg-secondary')
})
it.skip('should render destructive variant', () => {
// render(<Button variant="destructive">Delete</Button>)
// expect(screen.getByRole('button')).toHaveClass('bg-destructive')
})
it.skip('should render outline variant', () => {
// render(<Button variant="outline">Outline</Button>)
// expect(screen.getByRole('button')).toHaveClass('border')
})
it.skip('should render ghost variant', () => {
// render(<Button variant="ghost">Ghost</Button>)
// expect(screen.getByRole('button')).toHaveClass('hover:bg-accent')
})
})
describe('sizes', () => {
it.skip('should render default size', () => {
// render(<Button>Default</Button>)
// expect(screen.getByRole('button')).toHaveClass('h-10')
})
it.skip('should render small size', () => {
// render(<Button size="sm">Small</Button>)
// expect(screen.getByRole('button')).toHaveClass('h-8')
})
it.skip('should render large size', () => {
// render(<Button size="lg">Large</Button>)
// expect(screen.getByRole('button')).toHaveClass('h-12')
})
})
describe('with icons', () => {
it.skip('should render with left icon', () => {
// const Icon = () => <span data-testid="icon">icon</span>
// render(<Button leftIcon={<Icon />}>With Icon</Button>)
//
// expect(screen.getByTestId('icon')).toBeInTheDocument()
// expect(screen.getByText('With Icon')).toBeInTheDocument()
})
it.skip('should render with right icon', () => {
// const Icon = () => <span data-testid="icon">icon</span>
// render(<Button rightIcon={<Icon />}>With Icon</Button>)
//
// expect(screen.getByTestId('icon')).toBeInTheDocument()
})
it.skip('should render icon only button', () => {
// const Icon = () => <span data-testid="icon">icon</span>
// render(<Button size="icon"><Icon /></Button>)
//
// expect(screen.getByRole('button')).toHaveClass('h-10 w-10')
})
})
describe('accessibility', () => {
it.skip('should have correct role', () => {
// render(<Button>Button</Button>)
// expect(screen.getByRole('button')).toBeInTheDocument()
})
it.skip('should support aria-label', () => {
// render(<Button aria-label="Close dialog">X</Button>)
// expect(screen.getByRole('button')).toHaveAttribute('aria-label', 'Close dialog')
})
it.skip('should indicate loading state to screen readers', () => {
// render(<Button loading aria-busy>Loading</Button>)
// expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true')
})
})
})
@@ -0,0 +1,204 @@
/**
* 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()
// })
})
})
})
+300
View File
@@ -0,0 +1,300 @@
/**
* useVideoAudit Hook 单元测试
*
* TDD 测试用例 - 测试视频审核相关的自定义 Hook
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, waitFor, act } from '@testing-library/react'
// import { useVideoAudit } from './useVideoAudit'
// import { QueryClientProvider, QueryClient } from '@tanstack/react-query'
describe('useVideoAudit', () => {
// let queryClient: QueryClient
// let wrapper: React.FC<{ children: React.ReactNode }>
beforeEach(() => {
// queryClient = new QueryClient({
// defaultOptions: {
// queries: { retry: false },
// },
// })
// wrapper = ({ children }) => (
// <QueryClientProvider client={queryClient}>
// {children}
// </QueryClientProvider>
// )
})
it.skip('should fetch video audit data', async () => {
// const { result } = renderHook(
// () => useVideoAudit('video_001'),
// { wrapper }
// )
//
// expect(result.current.isLoading).toBe(true)
//
// await waitFor(() => {
// expect(result.current.isLoading).toBe(false)
// })
//
// expect(result.current.data).toBeDefined()
// expect(result.current.data?.video_id).toBe('video_001')
})
it.skip('should return violations', async () => {
// const { result } = renderHook(
// () => useVideoAudit('video_001'),
// { wrapper }
// )
//
// await waitFor(() => {
// expect(result.current.violations).toBeDefined()
// })
//
// expect(result.current.violations.length).toBeGreaterThan(0)
})
it.skip('should return brief compliance data', async () => {
// const { result } = renderHook(
// () => useVideoAudit('video_001'),
// { wrapper }
// )
//
// await waitFor(() => {
// expect(result.current.briefCompliance).toBeDefined()
// })
//
// expect(result.current.briefCompliance?.selling_points).toBeDefined()
})
it.skip('should handle error state', async () => {
// const { result } = renderHook(
// () => useVideoAudit('nonexistent_video'),
// { wrapper }
// )
//
// await waitFor(() => {
// expect(result.current.isError).toBe(true)
// })
//
// expect(result.current.error).toBeDefined()
})
describe('mutations', () => {
it.skip('should submit review decision', async () => {
// const { result } = renderHook(
// () => useVideoAudit('video_001'),
// { wrapper }
// )
//
// await waitFor(() => {
// expect(result.current.isLoading).toBe(false)
// })
//
// await act(async () => {
// await result.current.submitDecision({
// decision: 'passed',
// comment: '内容符合要求',
// })
// })
//
// expect(result.current.isSubmitting).toBe(false)
})
it.skip('should add manual violation', async () => {
// const { result } = renderHook(
// () => useVideoAudit('video_001'),
// { wrapper }
// )
//
// await waitFor(() => {
// expect(result.current.isLoading).toBe(false)
// })
//
// const initialCount = result.current.violations.length
//
// await act(async () => {
// await result.current.addViolation({
// type: 'other',
// content: '手动添加的问题',
// timestamp_start: 10.0,
// timestamp_end: 15.0,
// severity: 'medium',
// })
// })
//
// expect(result.current.violations.length).toBe(initialCount + 1)
})
it.skip('should delete violation', async () => {
// const { result } = renderHook(
// () => useVideoAudit('video_001'),
// { wrapper }
// )
//
// await waitFor(() => {
// expect(result.current.violations.length).toBeGreaterThan(0)
// })
//
// const initialCount = result.current.violations.length
//
// await act(async () => {
// await result.current.deleteViolation('vio_001')
// })
//
// expect(result.current.violations.length).toBe(initialCount - 1)
})
})
describe('optimistic updates', () => {
it.skip('should optimistically update violation selection', async () => {
// const { result } = renderHook(
// () => useVideoAudit('video_001'),
// { wrapper }
// )
//
// await waitFor(() => {
// expect(result.current.isLoading).toBe(false)
// })
//
// act(() => {
// result.current.toggleViolationSelection('vio_001')
// })
//
// expect(result.current.selectedViolationIds).toContain('vio_001')
})
it.skip('should rollback on mutation error', async () => {
// // 模拟 API 错误
// server.use(
// http.delete('/api/v1/violations/:id', () => {
// return new HttpResponse(null, { status: 500 })
// })
// )
//
// const { result } = renderHook(
// () => useVideoAudit('video_001'),
// { wrapper }
// )
//
// await waitFor(() => {
// expect(result.current.violations.length).toBeGreaterThan(0)
// })
//
// const initialCount = result.current.violations.length
//
// await act(async () => {
// try {
// await result.current.deleteViolation('vio_001')
// } catch (e) {
// // 预期的错误
// }
// })
//
// // 应该回滚到原始状态
// expect(result.current.violations.length).toBe(initialCount)
})
})
})
describe('useVideoPlayer', () => {
it.skip('should manage playback state', () => {
// const { result } = renderHook(() => useVideoPlayer())
//
// expect(result.current.isPlaying).toBe(false)
//
// act(() => {
// result.current.play()
// })
//
// expect(result.current.isPlaying).toBe(true)
//
// act(() => {
// result.current.pause()
// })
//
// expect(result.current.isPlaying).toBe(false)
})
it.skip('should manage current time', () => {
// const { result } = renderHook(() => useVideoPlayer())
//
// expect(result.current.currentTime).toBe(0)
//
// act(() => {
// result.current.seekTo(5000)
// })
//
// expect(result.current.currentTime).toBe(5000)
})
it.skip('should manage volume', () => {
// const { result } = renderHook(() => useVideoPlayer())
//
// expect(result.current.volume).toBe(1)
// expect(result.current.isMuted).toBe(false)
//
// act(() => {
// result.current.setVolume(0.5)
// })
//
// expect(result.current.volume).toBe(0.5)
//
// act(() => {
// result.current.toggleMute()
// })
//
// expect(result.current.isMuted).toBe(true)
})
})
describe('useAppeal', () => {
it.skip('should fetch appeal tokens', async () => {
// const { result } = renderHook(
// () => useAppeal(),
// { wrapper }
// )
//
// await waitFor(() => {
// expect(result.current.tokens).toBeDefined()
// })
//
// expect(result.current.tokens).toBeGreaterThanOrEqual(0)
})
it.skip('should check if appeal is available', async () => {
// const { result } = renderHook(
// () => useAppeal(),
// { wrapper }
// )
//
// await waitFor(() => {
// expect(result.current.canAppeal).toBeDefined()
// })
})
it.skip('should submit appeal', async () => {
// const { result } = renderHook(
// () => useAppeal(),
// { wrapper }
// )
//
// await act(async () => {
// await result.current.submitAppeal({
// videoId: 'video_001',
// violationIds: ['vio_001'],
// reason: '这个词语在此语境下是正常使用,不应被判定为违规',
// })
// })
//
// expect(result.current.isSubmitting).toBe(false)
})
it.skip('should validate appeal reason length', () => {
// const { result } = renderHook(() => useAppeal())
//
// expect(result.current.validateReason('短')).toBe(false)
// expect(result.current.validateReason('这是一个足够长的申诉理由')).toBe(true)
})
})
+176
View File
@@ -0,0 +1,176 @@
/**
* 工具函数单元测试
*
* TDD 测试用例 - 测试通用工具函数
*/
import { describe, it, expect } from 'vitest'
// 导入待实现的模块(TDD 红灯阶段)
// import {
// formatTimestamp,
// formatDuration,
// formatFileSize,
// truncateText,
// validateEmail,
// validatePassword,
// cn,
// } from './utils'
describe('formatTimestamp', () => {
it.skip('should format milliseconds to mm:ss format', () => {
// expect(formatTimestamp(0)).toBe('00:00')
// expect(formatTimestamp(5000)).toBe('00:05')
// expect(formatTimestamp(60000)).toBe('01:00')
// expect(formatTimestamp(90500)).toBe('01:30')
})
it.skip('should format to hh:mm:ss for long durations', () => {
// expect(formatTimestamp(3600000)).toBe('01:00:00')
// expect(formatTimestamp(3661000)).toBe('01:01:01')
})
it.skip('should handle negative values', () => {
// expect(formatTimestamp(-1000)).toBe('00:00')
})
})
describe('formatDuration', () => {
it.skip('should format seconds to human readable format', () => {
// expect(formatDuration(5)).toBe('5秒')
// expect(formatDuration(60)).toBe('1分钟')
// expect(formatDuration(90)).toBe('1分30秒')
// expect(formatDuration(3600)).toBe('1小时')
// expect(formatDuration(3661)).toBe('1小时1分1秒')
})
it.skip('should handle decimal values', () => {
// expect(formatDuration(5.5)).toBe('5.5秒')
})
})
describe('formatFileSize', () => {
it.skip('should format bytes to human readable format', () => {
// expect(formatFileSize(0)).toBe('0 B')
// expect(formatFileSize(500)).toBe('500 B')
// expect(formatFileSize(1024)).toBe('1 KB')
// expect(formatFileSize(1048576)).toBe('1 MB')
// expect(formatFileSize(1073741824)).toBe('1 GB')
})
it.skip('should handle decimal precision', () => {
// expect(formatFileSize(1536, 2)).toBe('1.50 KB')
})
})
describe('truncateText', () => {
it.skip('should truncate text exceeding max length', () => {
// expect(truncateText('Hello World', 5)).toBe('Hello...')
// expect(truncateText('Hello', 10)).toBe('Hello')
})
it.skip('should handle Chinese characters', () => {
// expect(truncateText('这是一段测试文字', 4)).toBe('这是一段...')
})
it.skip('should allow custom ellipsis', () => {
// expect(truncateText('Hello World', 5, '…')).toBe('Hello…')
})
})
describe('validateEmail', () => {
it.skip('should validate correct email formats', () => {
// expect(validateEmail('test@example.com')).toBe(true)
// expect(validateEmail('user.name@domain.co.jp')).toBe(true)
// expect(validateEmail('user+tag@example.com')).toBe(true)
})
it.skip('should reject invalid email formats', () => {
// expect(validateEmail('')).toBe(false)
// expect(validateEmail('invalid')).toBe(false)
// expect(validateEmail('invalid@')).toBe(false)
// expect(validateEmail('@domain.com')).toBe(false)
// expect(validateEmail('test@.com')).toBe(false)
})
})
describe('validatePassword', () => {
it.skip('should require minimum length', () => {
// const result = validatePassword('short')
// expect(result.isValid).toBe(false)
// expect(result.errors).toContain('密码长度至少 8 位')
})
it.skip('should require complexity', () => {
// const result = validatePassword('password')
// expect(result.isValid).toBe(false)
// expect(result.errors).toContain('密码需包含数字')
})
it.skip('should accept valid passwords', () => {
// const result = validatePassword('Password123!')
// expect(result.isValid).toBe(true)
// expect(result.errors).toHaveLength(0)
})
})
describe('cn (classnames utility)', () => {
it.skip('should merge class names', () => {
// expect(cn('foo', 'bar')).toBe('foo bar')
// expect(cn('foo', undefined, 'bar')).toBe('foo bar')
// expect(cn('foo', false && 'bar', 'baz')).toBe('foo baz')
})
it.skip('should handle tailwind class conflicts', () => {
// expect(cn('p-4', 'p-2')).toBe('p-2')
// expect(cn('text-red-500', 'text-blue-500')).toBe('text-blue-500')
})
})
describe('timestamp conversion', () => {
it.skip('should convert frame number to milliseconds', () => {
// expect(frameToMs(30, 30)).toBe(1000) // 30 frames at 30fps = 1 second
// expect(frameToMs(45, 30)).toBe(1500) // 45 frames at 30fps = 1.5 seconds
// expect(frameToMs(60, 60)).toBe(1000) // 60 frames at 60fps = 1 second
})
it.skip('should convert milliseconds to frame number', () => {
// expect(msToFrame(1000, 30)).toBe(30)
// expect(msToFrame(1500, 30)).toBe(45)
// expect(msToFrame(1000, 60)).toBe(60)
})
it.skip('should round frame numbers correctly', () => {
// expect(msToFrame(1033, 30)).toBe(31) // 1.033s * 30fps = 30.99 → 31
})
})
describe('severity helpers', () => {
it.skip('should return correct color for severity', () => {
// expect(getSeverityColor('high')).toBe('red')
// expect(getSeverityColor('medium')).toBe('orange')
// expect(getSeverityColor('low')).toBe('yellow')
})
it.skip('should return correct label for severity', () => {
// expect(getSeverityLabel('high')).toBe('高风险')
// expect(getSeverityLabel('medium')).toBe('中风险')
// expect(getSeverityLabel('low')).toBe('低风险')
})
})
describe('status helpers', () => {
it.skip('should return correct status color', () => {
// expect(getStatusColor('passed')).toBe('green')
// expect(getStatusColor('rejected')).toBe('red')
// expect(getStatusColor('pending_review')).toBe('orange')
// expect(getStatusColor('processing')).toBe('blue')
})
it.skip('should return correct status label', () => {
// expect(getStatusLabel('passed')).toBe('已通过')
// expect(getStatusLabel('rejected')).toBe('已驳回')
// expect(getStatusLabel('pending_review')).toBe('待审核')
// expect(getStatusLabel('processing')).toBe('处理中')
})
})
+154
View File
@@ -0,0 +1,154 @@
/**
* MSW 请求处理器
*
* 模拟后端 API 响应
*/
// import { http, HttpResponse } from 'msw'
// API Base URL
// const API_BASE = '/api/v1'
// 模拟用户数据
export const mockUsers = {
creator: {
id: 'user_creator_001',
email: 'creator@test.com',
name: '测试达人',
role: 'creator',
appeal_tokens: 3,
},
agency: {
id: 'user_agency_001',
email: 'agency@test.com',
name: '测试 Agency',
role: 'agency',
},
brand: {
id: 'user_brand_001',
email: 'brand@test.com',
name: '测试品牌方',
role: 'brand',
},
admin: {
id: 'user_admin_001',
email: 'admin@test.com',
name: '系统管理员',
role: 'admin',
},
}
// 模拟视频数据
export const mockVideos = [
{
id: 'video_001',
title: '测试视频 1',
status: 'pending_review',
creator_id: 'user_creator_001',
task_id: 'task_001',
duration_ms: 60000,
created_at: '2024-01-01T00:00:00Z',
},
{
id: 'video_002',
title: '测试视频 2',
status: 'passed',
creator_id: 'user_creator_001',
task_id: 'task_001',
duration_ms: 120000,
created_at: '2024-01-02T00:00:00Z',
},
]
// 模拟违规数据
export const mockViolations = [
{
id: 'vio_001',
video_id: 'video_001',
type: 'prohibited_word',
content: '最好的',
timestamp_start: 5.0,
timestamp_end: 5.5,
severity: 'high',
source: 'ai',
},
{
id: 'vio_002',
video_id: 'video_001',
type: 'competitor_logo',
content: 'CompetitorBrand',
timestamp_start: 10.0,
timestamp_end: 15.0,
severity: 'medium',
source: 'ai',
},
]
// 模拟 Brief 数据
export const mockBriefs = {
brief_001: {
id: 'brief_001',
task_id: 'task_001',
selling_points: [
{ text: '24小时持妆', priority: 'high' },
{ text: '天然成分', priority: 'medium' },
],
forbidden_words: ['药用', '治疗', '最好的'],
timing_requirements: [
{ type: 'product_visible', min_duration_seconds: 5 },
{ type: 'brand_mention', min_frequency: 3 },
],
brand_tone: {
style: ['年轻活力', '专业可信'],
target_audience: '18-35岁女性',
},
platform: 'douyin',
region: 'mainland_china',
},
}
// TODO: 实现 MSW handlers
// export const handlers = [
// // 认证相关
// http.post(`${API_BASE}/auth/login`, async ({ request }) => {
// const body = await request.json()
// // 模拟登录逻辑
// return HttpResponse.json({
// access_token: 'mock_token',
// user: mockUsers.creator,
// })
// }),
//
// // 视频相关
// http.get(`${API_BASE}/videos`, () => {
// return HttpResponse.json({
// items: mockVideos,
// total: mockVideos.length,
// page: 1,
// page_size: 10,
// })
// }),
//
// http.get(`${API_BASE}/videos/:videoId`, ({ params }) => {
// const video = mockVideos.find(v => v.id === params.videoId)
// if (!video) {
// return new HttpResponse(null, { status: 404 })
// }
// return HttpResponse.json(video)
// }),
//
// // 审核相关
// http.get(`${API_BASE}/videos/:videoId/violations`, ({ params }) => {
// const violations = mockViolations.filter(v => v.video_id === params.videoId)
// return HttpResponse.json({ violations })
// }),
//
// // Brief 相关
// http.get(`${API_BASE}/briefs/:briefId`, ({ params }) => {
// const brief = mockBriefs[params.briefId as keyof typeof mockBriefs]
// if (!brief) {
// return new HttpResponse(null, { status: 404 })
// }
// return HttpResponse.json(brief)
// }),
// ]
+73
View File
@@ -0,0 +1,73 @@
/**
* 前端测试全局设置
*
* 配置 MSW (Mock Service Worker) 和测试工具
*/
import '@testing-library/jest-dom'
import { afterAll, afterEach, beforeAll, vi } from 'vitest'
// import { server } from './mocks/server'
// MSW 服务器设置
// beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
// afterEach(() => server.resetHandlers())
// afterAll(() => server.close())
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})
// Mock IntersectionObserver
class MockIntersectionObserver implements IntersectionObserver {
readonly root: Element | null = null
readonly rootMargin: string = ''
readonly thresholds: ReadonlyArray<number> = []
constructor(
private callback: IntersectionObserverCallback,
_options?: IntersectionObserverInit
) {}
observe(_target: Element): void {}
unobserve(_target: Element): void {}
disconnect(): void {}
takeRecords(): IntersectionObserverEntry[] {
return []
}
}
window.IntersectionObserver = MockIntersectionObserver
// Mock ResizeObserver
class MockResizeObserver implements ResizeObserver {
constructor(_callback: ResizeObserverCallback) {}
observe(_target: Element, _options?: ResizeObserverOptions): void {}
unobserve(_target: Element): void {}
disconnect(): void {}
}
window.ResizeObserver = MockResizeObserver
// Mock URL.createObjectURL
URL.createObjectURL = vi.fn(() => 'mock-url')
URL.revokeObjectURL = vi.fn()
// Mock scrollTo
window.scrollTo = vi.fn()
// 清理 localStorage
afterEach(() => {
localStorage.clear()
sessionStorage.clear()
})
+40
View File
@@ -0,0 +1,40 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./tests/setup.ts'],
include: ['src/**/*.{test,spec}.{ts,tsx}', 'tests/**/*.{test,spec}.{ts,tsx}'],
exclude: ['node_modules', 'dist', 'e2e'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'tests/',
'**/*.d.ts',
'**/*.config.*',
'**/types/**',
],
thresholds: {
// 前端覆盖率目标 >= 70%
lines: 70,
branches: 70,
functions: 70,
statements: 70,
},
},
// 测试超时设置
testTimeout: 10000,
hookTimeout: 10000,
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})