/** * 模板库页面 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 }) }) })