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:
co-authored by
Claude Opus 4.5
parent
18fe22ce8a
commit
040aada160
@@ -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,
|
||||
},
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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() // 未达标
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
// })
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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('处理中')
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
// }),
|
||||
// ]
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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'),
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user