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
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
552 lines
18 KiB
TypeScript
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/)
|
|
})
|
|
})
|