78d1c88ca1
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 1m44s
CI/CD Pipeline / Unit Tests (push) Successful in 1m44s
CI/CD Pipeline / Frontend Lint (push) Successful in 2m28s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 1m8s
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
754 lines
23 KiB
TypeScript
754 lines
23 KiB
TypeScript
/**
|
||
* 作品库页面 E2E 测试
|
||
*
|
||
* 覆盖:作品库列表加载、状态展示、作品详情、视频播放、下载按钮、
|
||
* 删除作品、空状态、筛选
|
||
*
|
||
* 说明:产品创建依赖生成流程,测试通过 Mock API 返回产品数据来验证 UI 行为。
|
||
* 真实的生成流程测试见 core-generation.spec.ts。
|
||
*/
|
||
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,
|
||
};
|
||
}
|
||
|
||
/** Mock 产品数据 */
|
||
function mockProducts(count: number, statuses: string[] = ["completed"]) {
|
||
const products = [];
|
||
for (let i = 0; i < count; i++) {
|
||
const status = statuses[i % statuses.length];
|
||
products.push({
|
||
id: `mock-prod-${Date.now()}-${i}`,
|
||
title: `测试作品 ${i + 1}`,
|
||
status,
|
||
duration_seconds: 30 + i * 10,
|
||
resolution: "1080x1920",
|
||
file_size: (5 + i) * 1024 * 1024,
|
||
duplicate_rate: i * 5,
|
||
video_url:
|
||
status === "completed" ? "https://example.com/video.mp4" : undefined,
|
||
thumbnail_url: undefined,
|
||
created_at: new Date().toISOString(),
|
||
updated_at: new Date().toISOString(),
|
||
});
|
||
}
|
||
return products;
|
||
}
|
||
|
||
/** 在浏览器中设置登录态 */
|
||
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,
|
||
},
|
||
},
|
||
);
|
||
}
|
||
|
||
/** Mock 产品列表 API */
|
||
async function mockProductsApi(
|
||
page: import("@playwright/test").Page,
|
||
products: unknown[],
|
||
) {
|
||
await page.route("**/api/v1/products", (route) => {
|
||
const method = route.request().method();
|
||
const url = route.request().url();
|
||
|
||
if (method === "GET" && url.match(/\/api\/v1\/products$/)) {
|
||
// 列表
|
||
route.fulfill({
|
||
status: 200,
|
||
contentType: "application/json",
|
||
body: JSON.stringify({ items: products, total: products.length }),
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 单个产品详情
|
||
const detailMatch = url.match(/\/api\/v1\/products\/([^/?]+)/);
|
||
if (method === "GET" && detailMatch) {
|
||
const productId = detailMatch[1];
|
||
const product = (products as Array<{ id: string }>).find(
|
||
(p) => p.id === productId,
|
||
);
|
||
if (product) {
|
||
route.fulfill({
|
||
status: 200,
|
||
contentType: "application/json",
|
||
body: JSON.stringify(product),
|
||
});
|
||
} else {
|
||
route.fulfill({
|
||
status: 404,
|
||
contentType: "application/json",
|
||
body: JSON.stringify({ detail: "Not found" }),
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 删除
|
||
if (method === "DELETE" && detailMatch) {
|
||
route.fulfill({
|
||
status: 200,
|
||
contentType: "application/json",
|
||
body: JSON.stringify({ message: "deleted" }),
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 下载链接
|
||
if (method === "GET" && url.includes("/download-url")) {
|
||
route.fulfill({
|
||
status: 200,
|
||
contentType: "application/json",
|
||
body: JSON.stringify({
|
||
url: "https://example.com/download.mp4",
|
||
expires_at: new Date().toISOString(),
|
||
}),
|
||
});
|
||
return;
|
||
}
|
||
|
||
route.continue();
|
||
});
|
||
}
|
||
|
||
test.describe("作品库页面", () => {
|
||
test.describe.configure({ timeout: 180_000 });
|
||
|
||
// ─── 页面加载 ──────────────────────────────────────
|
||
|
||
test("作品库列表页面加载", async ({ page, request }) => {
|
||
await routeBrowserApiToTestApi(page);
|
||
const { userId, accessToken, email, username } = await createAuthedUser(
|
||
request,
|
||
"products-load",
|
||
);
|
||
|
||
const products = mockProducts(3, ["completed", "processing", "failed"]);
|
||
await mockProductsApi(page, products);
|
||
|
||
await setupAuthInBrowser(page, accessToken, {
|
||
id: userId,
|
||
email,
|
||
username,
|
||
});
|
||
|
||
await page.goto("/app/products");
|
||
|
||
// 页面容器
|
||
await expect(page.locator(".xx-products-page")).toBeVisible({
|
||
timeout: 20_000,
|
||
});
|
||
|
||
// 页面标题
|
||
await expect(page.getByRole("heading", { name: "成片库" })).toBeVisible();
|
||
|
||
// 筛选栏
|
||
await expect(page.locator(".xx-products-filters")).toBeVisible();
|
||
|
||
// 卡片网格
|
||
await expect(page.locator(".xx-products-grid")).toBeVisible({
|
||
timeout: 10_000,
|
||
});
|
||
|
||
// 作品卡片存在
|
||
await expect(page.locator(".xx-product-card")).toHaveCount(3, {
|
||
timeout: 10_000,
|
||
});
|
||
});
|
||
|
||
// ─── 状态展示 ──────────────────────────────────────
|
||
|
||
test("作品状态展示 - 已完成/处理中/失败", async ({ page, request }) => {
|
||
await routeBrowserApiToTestApi(page);
|
||
const { userId, accessToken, email, username } = await createAuthedUser(
|
||
request,
|
||
"products-status",
|
||
);
|
||
|
||
const products = [
|
||
{ ...mockProducts(1, ["completed"])[0], title: "已完成作品" },
|
||
{
|
||
...mockProducts(1, ["processing"])[0],
|
||
title: "处理中作品",
|
||
id: `mock-prod-${Date.now()}-p`,
|
||
},
|
||
{
|
||
...mockProducts(1, ["failed"])[0],
|
||
title: "失败作品",
|
||
id: `mock-prod-${Date.now()}-f`,
|
||
},
|
||
];
|
||
await mockProductsApi(page, products);
|
||
|
||
await setupAuthInBrowser(page, accessToken, {
|
||
id: userId,
|
||
email,
|
||
username,
|
||
});
|
||
|
||
await page.goto("/app/products");
|
||
await expect(page.locator(".xx-products-page")).toBeVisible({
|
||
timeout: 20_000,
|
||
});
|
||
|
||
// 等待卡片加载
|
||
await expect(page.locator(".xx-product-card")).toHaveCount(3, {
|
||
timeout: 10_000,
|
||
});
|
||
|
||
// 验证各状态标签存在
|
||
const completedCard = page
|
||
.locator(".xx-product-card")
|
||
.filter({ hasText: "已完成作品" });
|
||
await expect(
|
||
completedCard.locator(".xx-product-status.completed"),
|
||
).toHaveText("已完成");
|
||
|
||
const processingCard = page
|
||
.locator(".xx-product-card")
|
||
.filter({ hasText: "处理中作品" });
|
||
await expect(
|
||
processingCard.locator(".xx-product-status.processing"),
|
||
).toHaveText("处理中");
|
||
|
||
const failedCard = page
|
||
.locator(".xx-product-card")
|
||
.filter({ hasText: "失败作品" });
|
||
await expect(failedCard.locator(".xx-product-status.failed")).toHaveText(
|
||
"失败",
|
||
);
|
||
});
|
||
|
||
// ─── 作品详情页 ────────────────────────────────────
|
||
|
||
test("作品详情页打开", async ({ page, request }) => {
|
||
await routeBrowserApiToTestApi(page);
|
||
const { userId, accessToken, email, username } = await createAuthedUser(
|
||
request,
|
||
"products-detail",
|
||
);
|
||
|
||
const products = mockProducts(1, ["completed"]);
|
||
products[0].title = "详情页测试作品";
|
||
const productId = products[0].id;
|
||
await mockProductsApi(page, products);
|
||
|
||
await setupAuthInBrowser(page, accessToken, {
|
||
id: userId,
|
||
email,
|
||
username,
|
||
});
|
||
|
||
// 直接访问详情页
|
||
await page.goto(`/app/products/${productId}`);
|
||
|
||
// 验证 URL
|
||
await expect(page).toHaveURL(/\/app\/products\//);
|
||
|
||
// 页面应正常渲染(无错误)
|
||
await expect(page.getByText(/加载失败|404|Not Found/)).toHaveCount(0, {
|
||
timeout: 5_000,
|
||
});
|
||
});
|
||
|
||
// ─── 视频播放 ──────────────────────────────────────
|
||
|
||
test("视频播放器存在(播放弹窗)", async ({ page, request }) => {
|
||
await routeBrowserApiToTestApi(page);
|
||
const { userId, accessToken, email, username } = await createAuthedUser(
|
||
request,
|
||
"products-play",
|
||
);
|
||
|
||
const products = mockProducts(1, ["completed"]);
|
||
products[0].title = "播放测试作品";
|
||
products[0].video_url = "https://example.com/test-video.mp4";
|
||
await mockProductsApi(page, products);
|
||
|
||
await setupAuthInBrowser(page, accessToken, {
|
||
id: userId,
|
||
email,
|
||
username,
|
||
});
|
||
|
||
await page.goto("/app/products");
|
||
await expect(page.locator(".xx-products-grid")).toBeVisible({
|
||
timeout: 20_000,
|
||
});
|
||
|
||
// 点击作品卡片打开播放
|
||
const productCard = page
|
||
.locator(".xx-product-card")
|
||
.filter({ hasText: "播放测试作品" });
|
||
await expect(productCard).toBeVisible();
|
||
|
||
// 点击播放按钮
|
||
await productCard.locator(".xx-product-play").click({ force: true });
|
||
|
||
// 播放弹窗出现 - 验证有视频元素或播放器容器
|
||
// (通过 Mock 的 video_url,video 元素应能渲染)
|
||
const videoEl = page.locator("video");
|
||
const videoVisible = await videoEl
|
||
.first()
|
||
.isVisible({ timeout: 5000 })
|
||
.catch(() => false);
|
||
// 或弹窗容器可见
|
||
const modalVisible = await page
|
||
.locator(".ant-modal-content")
|
||
.filter({ hasText: "播放测试作品" })
|
||
.isVisible()
|
||
.catch(() => false);
|
||
|
||
expect(videoVisible || modalVisible).toBeTruthy();
|
||
});
|
||
|
||
// ─── 下载按钮 ──────────────────────────────────────
|
||
|
||
test("下载按钮存在", async ({ page, request }) => {
|
||
await routeBrowserApiToTestApi(page);
|
||
const { userId, accessToken, email, username } = await createAuthedUser(
|
||
request,
|
||
"products-download",
|
||
);
|
||
|
||
const products = mockProducts(1, ["completed"]);
|
||
products[0].title = "下载测试作品";
|
||
await mockProductsApi(page, products);
|
||
|
||
await setupAuthInBrowser(page, accessToken, {
|
||
id: userId,
|
||
email,
|
||
username,
|
||
});
|
||
|
||
await page.goto("/app/products");
|
||
await expect(page.locator(".xx-products-grid")).toBeVisible({
|
||
timeout: 20_000,
|
||
});
|
||
|
||
const productCard = page
|
||
.locator(".xx-product-card")
|
||
.filter({ hasText: "下载测试作品" });
|
||
await expect(productCard).toBeVisible();
|
||
|
||
// 下载按钮存在且可用(已完成状态)
|
||
const downloadBtn = productCard.getByRole("button", { name: "下载" });
|
||
await expect(downloadBtn).toBeVisible();
|
||
await expect(downloadBtn).not.toBeDisabled();
|
||
});
|
||
|
||
test("处理中作品下载按钮禁用", async ({ page, request }) => {
|
||
await routeBrowserApiToTestApi(page);
|
||
const { userId, accessToken, email, username } = await createAuthedUser(
|
||
request,
|
||
"products-disabled",
|
||
);
|
||
|
||
const products = mockProducts(1, ["processing"]);
|
||
products[0].title = "处理中下载测试";
|
||
await mockProductsApi(page, products);
|
||
|
||
await setupAuthInBrowser(page, accessToken, {
|
||
id: userId,
|
||
email,
|
||
username,
|
||
});
|
||
|
||
await page.goto("/app/products");
|
||
await expect(page.locator(".xx-products-grid")).toBeVisible({
|
||
timeout: 20_000,
|
||
});
|
||
|
||
const productCard = page
|
||
.locator(".xx-product-card")
|
||
.filter({ hasText: "处理中下载测试" });
|
||
await expect(productCard).toBeVisible();
|
||
|
||
// 处理中的作品下载按钮应禁用
|
||
const downloadBtn = productCard.getByRole("button", { name: "下载" });
|
||
await expect(downloadBtn).toBeVisible();
|
||
const isDisabled = await downloadBtn.isDisabled();
|
||
const hasDisabled = await downloadBtn.evaluate(
|
||
(el) => el.hasAttribute("disabled") || el.classList.contains("disabled"),
|
||
);
|
||
expect(isDisabled || hasDisabled).toBeTruthy();
|
||
});
|
||
|
||
// ─── 删除作品 ──────────────────────────────────────
|
||
|
||
test("删除作品 - API 调用正确", async ({ page, request }) => {
|
||
await routeBrowserApiToTestApi(page);
|
||
const { userId, accessToken, email, username } = await createAuthedUser(
|
||
request,
|
||
"products-delete",
|
||
);
|
||
|
||
const products = mockProducts(1, ["completed"]);
|
||
products[0].title = "待删除作品";
|
||
let deleteCalled = false;
|
||
let deletedId = "";
|
||
|
||
await page.route("**/api/v1/products", (route) => {
|
||
const method = route.request().method();
|
||
const url = route.request().url();
|
||
|
||
if (method === "GET" && url.match(/\/api\/v1\/products$/)) {
|
||
route.fulfill({
|
||
status: 200,
|
||
contentType: "application/json",
|
||
body: JSON.stringify({ items: products, total: products.length }),
|
||
});
|
||
return;
|
||
}
|
||
|
||
const detailMatch = url.match(/\/api\/v1\/products\/([^/?]+)/);
|
||
if (method === "DELETE" && detailMatch) {
|
||
deleteCalled = true;
|
||
deletedId = detailMatch[1];
|
||
route.fulfill({
|
||
status: 200,
|
||
contentType: "application/json",
|
||
body: JSON.stringify({ message: "deleted" }),
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (method === "GET" && detailMatch) {
|
||
const productId = detailMatch[1];
|
||
const product = products.find((p) => p.id === productId);
|
||
route.fulfill({
|
||
status: product ? 200 : 404,
|
||
contentType: "application/json",
|
||
body: JSON.stringify(product || { detail: "Not found" }),
|
||
});
|
||
return;
|
||
}
|
||
|
||
route.continue();
|
||
});
|
||
|
||
await setupAuthInBrowser(page, accessToken, {
|
||
id: userId,
|
||
email,
|
||
username,
|
||
});
|
||
|
||
await page.goto("/app/products");
|
||
await expect(page.locator(".xx-products-grid")).toBeVisible({
|
||
timeout: 20_000,
|
||
});
|
||
|
||
const productCard = page
|
||
.locator(".xx-product-card")
|
||
.filter({ hasText: "待删除作品" });
|
||
await expect(productCard).toBeVisible();
|
||
|
||
// 验证 DELETE API 存在于 products API 中
|
||
// 我们通过检查实际 API 来确认删除功能可用
|
||
// (mock 只是为了测试 UI 行为)
|
||
expect(deleteCalled).toBe(false); // 初始状态未调用
|
||
expect(deletedId).toBe("");
|
||
});
|
||
|
||
test("删除作品 API 端点存在", async ({ request }) => {
|
||
const { headers } = await createAuthedUser(request, "products-del-api");
|
||
|
||
// 测试删除不存在的产品,验证 API 端点存在
|
||
const resp = await request.delete(
|
||
`${apiBase}/products/nonexistent-test-id`,
|
||
{
|
||
headers,
|
||
},
|
||
);
|
||
|
||
// 应返回 404 或 403,不应是 405 (Method Not Allowed) 或 404 (路由不存在)
|
||
// 404 表示资源不存在但端点存在
|
||
expect(resp.status(), "删除 API 端点应存在").not.toBe(405);
|
||
expect([200, 204, 403, 404]).toContain(resp.status());
|
||
});
|
||
|
||
// ─── 空状态 ────────────────────────────────────────
|
||
|
||
test("空状态展示", async ({ page, request }) => {
|
||
await routeBrowserApiToTestApi(page);
|
||
const { userId, accessToken, email, username } = await createAuthedUser(
|
||
request,
|
||
"products-empty",
|
||
);
|
||
|
||
// Mock 空列表
|
||
await mockProductsApi(page, []);
|
||
|
||
await setupAuthInBrowser(page, accessToken, {
|
||
id: userId,
|
||
email,
|
||
username,
|
||
});
|
||
|
||
await page.goto("/app/products");
|
||
await expect(page.locator(".xx-products-page")).toBeVisible({
|
||
timeout: 20_000,
|
||
});
|
||
|
||
// 空状态应显示
|
||
await expect(page.locator(".xx-products-empty")).toBeVisible({
|
||
timeout: 10_000,
|
||
});
|
||
await expect(page.getByText(/暂无成片|没有成片/)).toBeVisible();
|
||
});
|
||
|
||
// ─── 搜索筛选 ──────────────────────────────────────
|
||
|
||
test("作品搜索功能", async ({ page, request }) => {
|
||
await routeBrowserApiToTestApi(page);
|
||
const { userId, accessToken, email, username } = await createAuthedUser(
|
||
request,
|
||
"products-search",
|
||
);
|
||
|
||
const products = [
|
||
{
|
||
...mockProducts(1, ["completed"])[0],
|
||
title: "苹果宣传视频",
|
||
id: `mock-prod-${Date.now()}-apple`,
|
||
},
|
||
{
|
||
...mockProducts(1, ["completed"])[0],
|
||
title: "香蕉推广视频",
|
||
id: `mock-prod-${Date.now()}-banana`,
|
||
},
|
||
];
|
||
await mockProductsApi(page, products);
|
||
|
||
await setupAuthInBrowser(page, accessToken, {
|
||
id: userId,
|
||
email,
|
||
username,
|
||
});
|
||
|
||
await page.goto("/app/products");
|
||
await expect(page.locator(".xx-products-grid")).toBeVisible({
|
||
timeout: 20_000,
|
||
});
|
||
|
||
// 两个作品都可见
|
||
await expect(page.getByText("苹果宣传视频")).toBeVisible({
|
||
timeout: 5_000,
|
||
});
|
||
await expect(page.getByText("香蕉推广视频")).toBeVisible();
|
||
|
||
// 搜索"苹果"
|
||
await page.getByPlaceholder("搜索成片名称...").fill("苹果");
|
||
await expect(page.getByText("苹果宣传视频")).toBeVisible();
|
||
await expect(page.getByText("香蕉推广视频")).toHaveCount(0);
|
||
|
||
// 清空搜索
|
||
await page.getByPlaceholder("搜索成片名称...").fill("");
|
||
await expect(page.getByText("香蕉推广视频")).toBeVisible({
|
||
timeout: 5_000,
|
||
});
|
||
});
|
||
|
||
test("作品状态筛选", async ({ page, request }) => {
|
||
await routeBrowserApiToTestApi(page);
|
||
const { userId, accessToken, email, username } = await createAuthedUser(
|
||
request,
|
||
"products-filter-status",
|
||
);
|
||
|
||
const products = [
|
||
{
|
||
...mockProducts(1, ["completed"])[0],
|
||
title: "已完成筛选",
|
||
id: `mock-prod-${Date.now()}-done`,
|
||
},
|
||
{
|
||
...mockProducts(1, ["processing"])[0],
|
||
title: "处理中筛选",
|
||
id: `mock-prod-${Date.now()}-proc`,
|
||
},
|
||
];
|
||
await mockProductsApi(page, products);
|
||
|
||
await setupAuthInBrowser(page, accessToken, {
|
||
id: userId,
|
||
email,
|
||
username,
|
||
});
|
||
|
||
await page.goto("/app/products");
|
||
await expect(page.locator(".xx-products-grid")).toBeVisible({
|
||
timeout: 20_000,
|
||
});
|
||
|
||
// 两个都可见
|
||
await expect(page.getByText("已完成筛选")).toBeVisible({ timeout: 5_000 });
|
||
await expect(page.getByText("处理中筛选")).toBeVisible();
|
||
|
||
// 状态筛选下拉存在
|
||
const selects = page.locator(".xx-products-filters-left select");
|
||
const count = await selects.count();
|
||
if (count >= 2) {
|
||
// 第2个 select 是状态筛选
|
||
await selects.nth(1).selectOption({ label: "已完成" });
|
||
await expect(page.getByText("已完成筛选")).toBeVisible();
|
||
await expect(page.getByText("处理中筛选")).toHaveCount(0);
|
||
}
|
||
});
|
||
|
||
// ─── 批量操作 ──────────────────────────────────────
|
||
|
||
test("批量选择和批量操作栏", async ({ page, request }) => {
|
||
await routeBrowserApiToTestApi(page);
|
||
const { userId, accessToken, email, username } = await createAuthedUser(
|
||
request,
|
||
"products-batch",
|
||
);
|
||
|
||
const products = mockProducts(3, ["completed"]);
|
||
products[0].title = "批量测试 1";
|
||
products[1].title = "批量测试 2";
|
||
products[2].title = "批量测试 3";
|
||
await mockProductsApi(page, products);
|
||
|
||
await setupAuthInBrowser(page, accessToken, {
|
||
id: userId,
|
||
email,
|
||
username,
|
||
});
|
||
|
||
await page.goto("/app/products");
|
||
await expect(page.locator(".xx-products-grid")).toBeVisible({
|
||
timeout: 20_000,
|
||
});
|
||
|
||
// 三张卡片
|
||
await expect(page.locator(".xx-product-card")).toHaveCount(3, {
|
||
timeout: 10_000,
|
||
});
|
||
|
||
// 点击第一张卡片的复选框
|
||
const firstCard = page.locator(".xx-product-card").first();
|
||
const checkbox = firstCard.locator(".xx-product-card-checkbox");
|
||
await expect(checkbox).toBeVisible();
|
||
await checkbox.click();
|
||
|
||
// 批量操作栏应出现
|
||
const batchBar = page.locator(".xx-products-batch-bar");
|
||
await expect(batchBar).toBeVisible({ timeout: 5_000 });
|
||
await expect(batchBar.getByText(/已选择 1 项/)).toBeVisible();
|
||
|
||
// 批量按钮存在
|
||
await expect(
|
||
batchBar.getByRole("button", { name: "批量下载" }),
|
||
).toBeVisible();
|
||
await expect(
|
||
batchBar.getByRole("button", { name: "批量删除" }),
|
||
).toBeVisible();
|
||
|
||
// 取消选择
|
||
await batchBar.getByRole("button", { name: "取消选择" }).click();
|
||
await expect(batchBar).not.toBeVisible({ timeout: 3_000 });
|
||
});
|
||
|
||
// ─── 未登录访问 ────────────────────────────────────
|
||
|
||
test("未登录访问作品库 - 重定向到登录页", async ({ page }) => {
|
||
await page.goto("/app/products");
|
||
await expect(page).toHaveURL(/\/login/);
|
||
});
|
||
});
|