/** * 模板库页面 E2E 测试 * * 覆盖:模板列表加载、分类切换、模板详情、收藏/取消收藏、 * 使用模板入口、搜索功能、我的模板tab、未登录重定向 * * 每个测试独立,先注册登录获取 auth token。 */ import { expect, test, type APIRequestContext, type Page, } from "@playwright/test"; const PASSWORD = "Test123456!"; 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: 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, }; } /** 设置页面认证状态(localStorage) */ async function setupAuth( page: Page, token: string, user: { id: string; email: string; username: string; display_name: 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.display_name, is_email_verified: true, email_verified: true, }, }, ); } test.describe("模板库页面 - 未登录重定向", () => { test("未登录访问重定向到登录页", async ({ page }) => { await page.goto("/app/templates"); await expect(page).toHaveURL(/\/login/); }); }); test.describe("模板库页面 - 页面加载", () => { test.describe.configure({ timeout: 120_000 }); test("模板库页面加载成功", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username } = await createAuthedUser( request, "tpl-load", ); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E tpl-load", }); await page.goto("/app/templates"); await expect(page.locator(".xx-templates-page")).toBeVisible({ timeout: 20_000, }); }); test("模板库头部和搜索栏存在", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username } = await createAuthedUser( request, "tpl-head", ); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E tpl-head", }); await page.goto("/app/templates"); await expect(page.locator(".xx-templates-page")).toBeVisible({ timeout: 20_000, }); // 验证搜索框 const searchInput = page.locator(".xx-templates-search-input"); await expect(searchInput).toBeVisible({ timeout: 10_000 }); }); test("分类切换按钮存在", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username } = await createAuthedUser( request, "tpl-cat", ); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E tpl-cat", }); await page.goto("/app/templates"); await expect(page.locator(".xx-templates-page")).toBeVisible({ timeout: 20_000, }); // 验证分类按钮存在 const categoryBtns = page.locator(".xx-templates-cat-btn"); await expect(categoryBtns.first()).toBeVisible({ timeout: 10_000 }); const count = await categoryBtns.count(); expect(count).toBeGreaterThan(0); }); }); test.describe("模板库 - 模板展示", () => { test.describe.configure({ timeout: 120_000 }); test("模板卡片展示", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username, headers } = await createAuthedUser(request, "tpl-cards"); const suffix = Date.now().toString(36); // 创建一个模板 await request.post(`${apiBase}/templates`, { headers, data: { name: `E2E 模板展示 ${suffix}`, mode: "pip", estimated_duration: 30, description: "测试模板展示", segments: [ { segment_order: 1, duration_min: 5, duration_max: 15, material_type: "video", }, ], tags: ["e2e", "展示"], category: "种草", }, }); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E tpl-cards", }); await page.goto("/app/templates"); await expect(page.locator(".xx-templates-page")).toBeVisible({ timeout: 20_000, }); // 等待模板卡片出现 const cards = page.locator(".xx-template-card"); await expect(cards.first()).toBeVisible({ timeout: 15_000 }); const count = await cards.count(); expect(count).toBeGreaterThan(0); }); test("模板卡片包含名称和类型", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username, headers } = await createAuthedUser(request, "tpl-info"); const suffix = Date.now().toString(36); await request.post(`${apiBase}/templates`, { headers, data: { name: `模板信息测试 ${suffix}`, mode: "voice_over", estimated_duration: 60, description: "测试信息展示", segments: [ { segment_order: 1, duration_min: 10, duration_max: 30, material_type: "video", }, ], }, }); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E tpl-info", }); await page.goto("/app/templates"); await expect(page.locator(".xx-templates-page")).toBeVisible({ timeout: 20_000, }); const firstCard = page.locator(".xx-template-card").first(); if (await firstCard.isVisible({ timeout: 15_000 })) { // 验证信息区域存在 const info = firstCard.locator(".xx-template-info"); await expect(info).toBeVisible(); } }); test("模板预览弹窗功能", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username, headers } = await createAuthedUser(request, "tpl-preview"); const suffix = Date.now().toString(36); await request.post(`${apiBase}/templates`, { headers, data: { name: `预览测试模板 ${suffix}`, mode: "pip", estimated_duration: 30, description: "预览测试描述", segments: [ { segment_order: 1, duration_min: 5, duration_max: 15, material_type: "video", description: "片段一", }, ], }, }); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E tpl-preview", }); await page.goto("/app/templates"); await expect(page.locator(".xx-templates-page")).toBeVisible({ timeout: 20_000, }); // 点击第一个模板卡片打开预览 const firstCard = page.locator(".xx-template-card").first(); if (await firstCard.isVisible({ timeout: 15_000 })) { await firstCard.click(); // 预览弹窗应该出现 const modal = page.locator(".xx-template-modal"); if (await modal.isVisible({ timeout: 5_000 })) { await expect(modal).toBeVisible(); // 验证预览内容存在 await expect( modal.locator(".xx-template-modal-title-row"), ).toBeVisible(); } } }); }); test.describe("模板库 - 分类切换", () => { test.describe.configure({ timeout: 120_000 }); test("切换分类筛选", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username } = await createAuthedUser( request, "tpl-switch", ); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E tpl-switch", }); await page.goto("/app/templates"); await expect(page.locator(".xx-templates-page")).toBeVisible({ timeout: 20_000, }); const categoryBtns = page.locator(".xx-templates-cat-btn"); const firstBtn = categoryBtns.first(); if (await firstBtn.isVisible({ timeout: 10_000 })) { await firstBtn.click(); // 验证按钮被选中 await expect(firstBtn).toHaveClass(/active/); } }); }); test.describe("模板库 - 搜索", () => { test.describe.configure({ timeout: 120_000 }); test("搜索框可输入并筛选", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username, headers } = await createAuthedUser(request, "tpl-search"); const suffix = Date.now().toString(36); const templateName = `E2E 搜索测试模板 ${suffix}`; await request.post(`${apiBase}/templates`, { headers, data: { name: templateName, mode: "pip", estimated_duration: 30, description: "搜索测试专用模板", segments: [ { segment_order: 1, duration_min: 5, duration_max: 15, material_type: "video", }, ], }, }); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E tpl-search", }); await page.goto("/app/templates"); await expect(page.locator(".xx-templates-page")).toBeVisible({ timeout: 20_000, }); const searchInput = page.locator(".xx-templates-search-input"); if (await searchInput.isVisible({ timeout: 10_000 })) { await searchInput.fill(suffix); // 验证页面正常响应 await expect(page.locator(".xx-templates-page")).toBeVisible(); } }); }); test.describe("模板库 - API 操作", () => { test.describe.configure({ timeout: 120_000 }); test("获取模板列表 - 正向", async ({ request }) => { const { headers } = await createAuthedUser(request, "tpl-api-list"); const suffix = Date.now().toString(36); await request.post(`${apiBase}/templates`, { headers, data: { name: `API 列表测试 ${suffix}`, mode: "pip", estimated_duration: 30, segments: [ { segment_order: 1, duration_min: 5, duration_max: 15, material_type: "video", }, ], }, }); const response = await request.get(`${apiBase}/templates`, { headers }); expect( response.ok(), `获取模板列表应返回 2xx,实际: ${response.status()}`, ).toBeTruthy(); const data = await response.json(); const items = data.items || data.templates || []; expect(Array.isArray(items), "模板列表应为数组").toBeTruthy(); expect(items.length).toBeGreaterThan(0); }); test("收藏/取消收藏模板 - 正向", async ({ request }) => { const { headers } = await createAuthedUser(request, "tpl-fav"); const suffix = Date.now().toString(36); // 创建模板 const createResp = await request.post(`${apiBase}/templates`, { headers, data: { name: `收藏测试 ${suffix}`, mode: "pip", estimated_duration: 30, segments: [ { segment_order: 1, duration_min: 5, duration_max: 15, material_type: "video", }, ], }, }); expect(createResp.ok()).toBeTruthy(); const created = await createResp.json(); const templateId = created.id; // 收藏 const favResp = await request.post( `${apiBase}/templates/${templateId}/favorite`, { headers }, ); // 收藏可能成功或接口不存在 expect(favResp.status() < 500, "收藏请求应返回 2xx 或 4xx").toBeTruthy(); // 取消收藏 const unfavResp = await request.delete( `${apiBase}/templates/${templateId}/favorite`, { headers }, ); expect( unfavResp.status() < 500, "取消收藏请求应返回 2xx 或 4xx", ).toBeTruthy(); }); test("获取模板详情 - 正向", async ({ request }) => { const { headers } = await createAuthedUser(request, "tpl-api-detail"); const suffix = Date.now().toString(36); const createResp = await request.post(`${apiBase}/templates`, { headers, data: { name: `详情测试 ${suffix}`, mode: "voice_over", estimated_duration: 60, description: "详情测试描述", segments: [ { segment_order: 1, duration_min: 10, duration_max: 30, material_type: "video", description: "测试片段", }, ], }, }); expect(createResp.ok()).toBeTruthy(); const created = await createResp.json(); const detailResp = await request.get(`${apiBase}/templates/${created.id}`, { headers, }); expect(detailResp.ok(), "获取详情应成功").toBeTruthy(); const detail = await detailResp.json(); expect(detail.id).toBe(created.id); expect(detail.name).toBe(`详情测试 ${suffix}`); }); test("使用模板接口 - 正向", async ({ request }) => { const { headers } = await createAuthedUser(request, "tpl-use"); const suffix = Date.now().toString(36); const createResp = await request.post(`${apiBase}/templates`, { headers, data: { name: `使用测试 ${suffix}`, mode: "pip", estimated_duration: 30, segments: [ { segment_order: 1, duration_min: 5, duration_max: 15, material_type: "video", }, ], }, }); expect(createResp.ok()).toBeTruthy(); const created = await createResp.json(); // 使用模板(生成) const genResp = await request.post( `${apiBase}/templates/${created.id}/generate`, { headers, data: {} }, ); // 生成可能成功或返回业务错误 expect(genResp.status() < 500, "使用模板应返回 2xx 或 4xx").toBeTruthy(); }); test("未登录获取模板列表 - 反向", async ({ request }) => { const response = await request.get(`${apiBase}/templates`); expect([401, 403]).toContain(response.status()); }); }); test.describe("模板库 - 我的模板 Tab", () => { test.describe.configure({ timeout: 120_000 }); test("我的模板页面可访问", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username } = await createAuthedUser( request, "tpl-my", ); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E tpl-my", }); await page.goto("/app/my-templates"); await expect(page.locator(".mt-page")).toBeVisible({ timeout: 20_000, }); }); test("我的模板页面展示已创建的模板", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username, headers } = await createAuthedUser(request, "tpl-my-data"); const suffix = Date.now().toString(36); await request.post(`${apiBase}/templates`, { headers, data: { name: `我的模板测试 ${suffix}`, mode: "pip", estimated_duration: 30, description: "我的模板展示测试", segments: [ { segment_order: 1, duration_min: 5, duration_max: 15, material_type: "video", }, ], }, }); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E tpl-my-data", }); await page.goto("/app/my-templates"); await expect(page.locator(".mt-page")).toBeVisible({ timeout: 20_000, }); // 验证卡片容器存在 const cards = page.locator(".mt-card"); await expect(cards.first()).toBeVisible({ timeout: 15_000 }); }); });