Files
xiaoxia-saas/apps/web/e2e/duplication.spec.ts
T
xiaoxia 0fcb77b991
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 1m6s
CI Build & Deploy Pipeline / Build Staging Web Image (push) Successful in 1m49s
CI Build & Deploy Pipeline / Build Production API Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (push) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (push) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 3m1s
CI/CD Pipeline / Unit Tests (push) Successful in 3m20s
CI/CD Pipeline / Integration Tests (push) Successful in 1m29s
CI Build & Deploy Pipeline / Build Staging API Image (push) Successful in 7m4s
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Successful in 7m54s
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 45s
CI Build & Deploy Pipeline / Staging E2E Tests (push) Failing after 2m50s
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Successful in 3m28s
ci: Prettier纳入两层防御体系 (#520)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-07-18 18:08:19 +08:00

552 lines
18 KiB
TypeScript

/**
* 去重流程 E2E 测试
*
* 覆盖:去重上传页面、上传区域、去重记录列表、去重详情、
* 删除记录、重试去重
*/
import { expect, test, type APIRequestContext } from "@playwright/test"
const PASSWORD = "SmokePass123!"
const apiBase = process.env.E2E_API_BASE || "/api/v1"
const apiOrigin = apiBase.endsWith("/api/v1") ? apiBase.slice(0, -"/api/v1".length) : ""
const routeBrowserApiToTestApi = async (page: import("@playwright/test").Page) => {
if (!apiOrigin) return
await page.route("**/api/v1/**", async (route) => {
const sourceUrl = new URL(route.request().url())
const response = await route.fetch({
url: `${apiOrigin}${sourceUrl.pathname}${sourceUrl.search}`,
})
await route.fulfill({ response })
})
}
function uniqueEmail(prefix: string): string {
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}@example.com`
}
function uniqueUsername(prefix: string): string {
return `${prefix}_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`
}
/** 登录操作,遇到 429 限流自动等待重试 */
async function loginWithRetry(
request: APIRequestContext,
email: string,
password: string,
maxRetries = 2,
) {
for (let i = 0; i <= maxRetries; i++) {
const response = await request.post(`${apiBase}/auth/login`, {
data: { email, password },
})
if (response.status() !== 429) return response
console.log(`[login] 触发限流,等待 65s 后重试 (${i + 1}/${maxRetries})`)
await new Promise((r) => setTimeout(r, 65000))
}
return request.post(`${apiBase}/auth/login`, {
data: { email, password },
})
}
/** 注册并登录,返回 { headers, email, username, userId, accessToken } */
async function createAuthedUser(request: APIRequestContext, label: string) {
const email = uniqueEmail(label)
const username = uniqueUsername(label)
const reg = await request.post(`${apiBase}/auth/register`, {
data: { email, password: PASSWORD, username, display_name: `E2E ${label}` },
})
expect(reg.ok(), `注册应成功: ${await reg.text()}`).toBeTruthy()
const regData = await reg.json()
const login = await loginWithRetry(request, email, PASSWORD)
expect(login.ok(), `登录应成功: ${await login.text()}`).toBeTruthy()
const loginData = await login.json()
return {
headers: { Authorization: `Bearer ${loginData.access_token}` },
email,
username,
userId: regData.user_id,
accessToken: loginData.access_token,
}
}
/** 在浏览器中设置登录态 */
async function setupAuthInBrowser(
page: import("@playwright/test").Page,
token: string,
user: { id: string; email: string; username: string },
) {
await page.addInitScript(
({ token, user }) => {
localStorage.setItem("access_token", token)
localStorage.setItem(
"auth-storage",
JSON.stringify({
state: { user, isAuthenticated: true },
version: 0,
}),
)
},
{
token,
user: {
id: user.id,
user_id: user.id,
email: user.email,
username: user.username,
display_name: user.username,
is_email_verified: true,
email_verified: true,
},
},
)
}
test.describe("去重流程", () => {
test.describe.configure({ timeout: 180_000 })
// ─── 上传页面加载 ──────────────────────────────────
test("去重上传页面加载", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { userId, accessToken, email, username } = await createAuthedUser(request, "dup-load")
await setupAuthInBrowser(page, accessToken, {
id: userId,
email,
username,
})
await page.goto("/app/duplication")
// 页面容器
await expect(page.locator(".dup-page")).toBeVisible({ timeout: 20_000 })
// 页面标题
await expect(page.getByRole("heading", { name: "视频查重" })).toBeVisible()
// 描述
await expect(page.getByText("上传视频文件,系统将自动检测与已有素材的重复片段")).toBeVisible()
})
// ─── 上传区域展示 ──────────────────────────────────
test("上传区域展示", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { userId, accessToken, email, username } = await createAuthedUser(
request,
"dup-upload-zone",
)
await setupAuthInBrowser(page, accessToken, {
id: userId,
email,
username,
})
await page.goto("/app/duplication")
await expect(page.locator(".dup-page")).toBeVisible({ timeout: 20_000 })
// 拖拽上传区域
const uploadZone = page.locator(".dup-upload-zone")
await expect(uploadZone).toBeVisible()
// 上传图标和文字
await expect(uploadZone.getByText("点击或拖拽视频文件到此区域")).toBeVisible()
// 格式提示
await expect(uploadZone.getByText(/支持 MP4、AVI、MOV、MKV/)).toBeVisible()
// 格式标签
await expect(page.locator(".dup-upload-formats")).toBeVisible()
// 选择文件按钮
const selectBtn = page.getByRole("button", { name: "选择文件" })
await expect(selectBtn).toBeVisible()
// 隐藏的文件 input
const fileInput = page.locator('input[type="file"]')
await expect(fileInput).toHaveCount(1)
})
// ─── 格式说明区 ────────────────────────────────────
test("格式说明和提示区域展示", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { userId, accessToken, email, username } = await createAuthedUser(request, "dup-info")
await setupAuthInBrowser(page, accessToken, {
id: userId,
email,
username,
})
await page.goto("/app/duplication")
await expect(page.locator(".dup-page")).toBeVisible({ timeout: 20_000 })
// 右侧说明区
const infoCard = page.locator(".dup-info-card")
await expect(infoCard).toBeVisible()
// 查重说明
await expect(infoCard.getByText("查重说明")).toBeVisible()
// 支持格式
await expect(infoCard.getByText("支持格式")).toBeVisible()
// 温馨提示
await expect(infoCard.getByText("温馨提示")).toBeVisible()
// 格式标签
await expect(page.locator(".dup-format-tags")).toBeVisible()
})
// ─── 去重记录列表页面 ──────────────────────────────
test("去重记录列表页面加载", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { userId, accessToken, email, username } = await createAuthedUser(request, "dup-list")
await setupAuthInBrowser(page, accessToken, {
id: userId,
email,
username,
})
await page.goto("/app/duplication/results")
// 页面容器
await expect(page.locator(".dup-page")).toBeVisible({ timeout: 20_000 })
// 页面标题
await expect(page.getByRole("heading", { name: "查重记录" })).toBeVisible()
// 筛选按钮
await expect(page.locator(".dup-filter")).toBeVisible()
// 上传查重按钮
await expect(page.getByRole("button", { name: "上传查重" })).toBeVisible()
})
test("去重记录列表 - 空状态", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { userId, accessToken, email, username } = await createAuthedUser(
request,
"dup-list-empty",
)
await setupAuthInBrowser(page, accessToken, {
id: userId,
email,
username,
})
await page.goto("/app/duplication/results")
await expect(page.locator(".dup-page")).toBeVisible({ timeout: 20_000 })
// 空状态(新用户没有记录)
const emptyState = page.locator(".dup-results-empty")
await expect(emptyState).toBeVisible({ timeout: 10_000 })
await expect(emptyState.getByText(/暂无查重记录/)).toBeVisible()
})
test("去重记录列表 - 风险等级筛选", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { userId, accessToken, email, username } = await createAuthedUser(request, "dup-filter")
await setupAuthInBrowser(page, accessToken, {
id: userId,
email,
username,
})
await page.goto("/app/duplication/results")
await expect(page.locator(".dup-page")).toBeVisible({ timeout: 20_000 })
// 筛选按钮存在
const filterBtns = page.locator(".dup-filter-btn")
await expect(filterBtns).toHaveCount(4) // 全部、低风险、中风险、高风险
// 验证按钮文本
await expect(filterBtns.nth(0)).toHaveText("全部")
await expect(filterBtns.nth(1)).toHaveText("低风险")
await expect(filterBtns.nth(2)).toHaveText("中风险")
await expect(filterBtns.nth(3)).toHaveText("高风险")
// 默认选中"全部"
await expect(filterBtns.nth(0)).toHaveClass(/active/)
// 点击低风险
await filterBtns.nth(1).click()
await expect(filterBtns.nth(1)).toHaveClass(/active/)
})
test("去重记录列表 - 上传查重按钮跳转", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { userId, accessToken, email, username } = await createAuthedUser(request, "dup-nav")
await setupAuthInBrowser(page, accessToken, {
id: userId,
email,
username,
})
await page.goto("/app/duplication/results")
await expect(page.locator(".dup-page")).toBeVisible({ timeout: 20_000 })
// 点击上传查重按钮
await page.getByRole("button", { name: "上传查重" }).click()
await expect(page).toHaveURL(/\/app\/duplication$/)
await expect(page.locator(".dup-upload-zone")).toBeVisible()
})
// ─── 去重详情页 ────────────────────────────────────
test("去重详情页 - 通过 API 创建测试数据后访问", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { headers, userId, accessToken, email, username } = await createAuthedUser(
request,
"dup-detail",
)
// 先上传一个文件进行查重,获取 record id
const uploadResp = await request.post(`${apiBase}/duplication/upload`, {
headers,
multipart: {
file: {
name: "e2e_dup_test.mp4",
mimeType: "video/mp4",
buffer: Buffer.from("e2e duplication test data"),
},
},
})
// 如果查重 API 不可用,跳过详情页测试
if (!uploadResp.ok()) {
console.log(`[skip] 查重上传 API 不可用 (${uploadResp.status()}),跳过详情页测试`)
return
}
const uploadData = await uploadResp.json()
const recordId = uploadData.id
expect(recordId, "应返回查重记录 ID").toBeTruthy()
await setupAuthInBrowser(page, accessToken, {
id: userId,
email,
username,
})
// 访问详情页
await page.goto(`/app/duplication/${recordId}`)
// 页面应正常渲染
await expect(page.locator(".dup-page")).toBeVisible({ timeout: 20_000 })
// 验证无错误
await expect(page.getByText(/加载失败|404|Not Found/)).toHaveCount(0, {
timeout: 5_000,
})
})
// ─── 删除记录 ──────────────────────────────────────
test("去重记录删除 - API 验证", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { headers } = await createAuthedUser(request, "dup-delete")
// 创建查重记录
const uploadResp = await request.post(`${apiBase}/duplication/upload`, {
headers,
multipart: {
file: {
name: "e2e_dup_delete.mp4",
mimeType: "video/mp4",
buffer: Buffer.from("e2e duplication delete test"),
},
},
})
if (!uploadResp.ok()) {
console.log(`[skip] 查重上传 API 不可用 (${uploadResp.status()}),跳过删除测试`)
return
}
const uploadData = await uploadResp.json()
const recordId = uploadData.id
// 验证记录存在
const listResp = await request.get(`${apiBase}/duplication/records`, {
headers,
})
if (listResp.ok()) {
const records = await listResp.json()
const recordExists = Array.isArray(records)
? records.some((r: { id: string }) => r.id === recordId)
: (records.items || []).some((r: { id: string }) => r.id === recordId)
expect(recordExists, "记录应存在于列表中").toBeTruthy()
}
// 删除记录
const deleteResp = await request.delete(`${apiBase}/duplication/records/${recordId}`, {
headers,
})
expect(deleteResp.ok(), `删除查重记录应成功: ${deleteResp.status()}`).toBeTruthy()
// 验证记录已删除
const listAfterResp = await request.get(`${apiBase}/duplication/records`, {
headers,
})
if (listAfterResp.ok()) {
const recordsAfter = await listAfterResp.json()
const recordStillExists = Array.isArray(recordsAfter)
? recordsAfter.some((r: { id: string }) => r.id === recordId)
: (recordsAfter.items || []).some((r: { id: string }) => r.id === recordId)
expect(recordStillExists, "记录应已被删除").toBeFalsy()
}
})
test("去重记录删除 - UI 验证", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { headers, userId, accessToken, email, username } = await createAuthedUser(
request,
"dup-delete-ui",
)
// 创建查重记录
const uploadResp = await request.post(`${apiBase}/duplication/upload`, {
headers,
multipart: {
file: {
name: "e2e_dup_ui_delete.mp4",
mimeType: "video/mp4",
buffer: Buffer.from("e2e duplication ui delete test"),
},
},
})
if (!uploadResp.ok()) {
console.log(`[skip] 查重上传 API 不可用 (${uploadResp.status()}),跳过 UI 删除测试`)
return
}
await setupAuthInBrowser(page, accessToken, {
id: userId,
email,
username,
})
await page.goto("/app/duplication/results")
await expect(page.locator(".dup-page")).toBeVisible({ timeout: 20_000 })
// 记录卡片应存在
const resultCard = page.locator(".dup-result-card").first()
const cardVisible = await resultCard.isVisible({ timeout: 10_000 }).catch(() => false)
if (cardVisible) {
// 删除按钮存在
const deleteBtn = resultCard.getByRole("button").filter({
hasText: "🗑️",
})
await expect(deleteBtn).toBeVisible()
// 删除按钮点击 - 会触发 confirm 对话框
// 这里我们通过监听 confirm 来确认删除
page.once("dialog", async (dialog) => {
expect(dialog.message()).toContain("确定删除")
await dialog.accept()
})
// 监听删除请求
const deletePromise = page
.waitForResponse(
(resp) =>
resp.url().includes("/duplication/records/") && resp.request().method() === "DELETE",
{ timeout: 10_000 },
)
.catch(() => null)
await deleteBtn.click()
const deleteResp = await deletePromise
if (deleteResp) {
expect(deleteResp.ok(), "删除请求应成功").toBeTruthy()
}
}
})
// ─── 重试去重 ──────────────────────────────────────
test("重试去重按钮 - 失败记录显示重试", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { headers, userId, accessToken, email, username } = await createAuthedUser(
request,
"dup-retry",
)
// 创建查重记录
const uploadResp = await request.post(`${apiBase}/duplication/upload`, {
headers,
multipart: {
file: {
name: "e2e_dup_retry.mp4",
mimeType: "video/mp4",
buffer: Buffer.from("e2e duplication retry test"),
},
},
})
if (!uploadResp.ok()) {
console.log(`[skip] 查重上传 API 不可用 (${uploadResp.status()}),跳过重试测试`)
return
}
await setupAuthInBrowser(page, accessToken, {
id: userId,
email,
username,
})
await page.goto("/app/duplication/results")
await expect(page.locator(".dup-page")).toBeVisible({ timeout: 20_000 })
// 记录列表中至少有一条记录
const resultCard = page.locator(".dup-result-card").first()
const cardVisible = await resultCard.isVisible({ timeout: 10_000 }).catch(() => false)
if (cardVisible) {
// 验证记录卡片基本结构
await expect(resultCard.locator(".dup-result-card-body")).toBeVisible()
await expect(resultCard.locator(".dup-result-card-score")).toBeVisible()
// 检查是否有重试按钮(失败状态才显示)
// 新上传的记录可能是处理中或完成状态,不一定显示重试按钮
// 这里只验证 API 重试接口可用
const uploadData = await uploadResp.json()
const recordId = uploadData.id
const retryResp = await request.post(`${apiBase}/duplication/records/${recordId}/retry`, {
headers,
})
// 重试接口应返回 2xx 或明确的状态码
expect(retryResp.status()).toBeLessThan(500)
}
})
// ─── 未登录访问 ────────────────────────────────────
test("未登录访问去重上传页 - 重定向到登录页", async ({ page }) => {
await page.goto("/app/duplication")
await expect(page).toHaveURL(/\/login/)
})
test("未登录访问去重记录页 - 重定向到登录页", async ({ page }) => {
await page.goto("/app/duplication/results")
await expect(page).toHaveURL(/\/login/)
})
})