ed446e5e51
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 2h13m59s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 2h13m59s
- register.spec.ts: 注册页面渲染、表单验证、成功注册、跳转链接(13个测试) - assets-full.spec.ts: 素材库完整交互(创建/切换/搜索/详情/删除/批量删除/空状态)(10个测试) - products.spec.ts: 作品库页面(列表/状态/详情/播放/下载/删除/空状态/筛选/批量)(15个测试) - duplication.spec.ts: 去重流程(上传页面/记录列表/详情/删除/重试)(13个测试) 总计48个测试用例,遵循现有测试风格,使用API创建测试数据,支持幂等安全
555 lines
18 KiB
TypeScript
555 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, userId, accessToken, email, username } = 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/);
|
|
});
|
|
});
|