import { expect, test, type APIRequestContext, type Page } 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) : "" async function routeBrowserApiToTestApi(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 }) }) } async function loginWithRetry(request: APIRequestContext, email: string, password: string) { for (let i = 0; i <= 2; i++) { const r = await request.post(`${apiBase}/auth/login`, { data: { email, password } }) if (r.status() !== 429) { expect(r.ok(), `login: ${await r.text()}`).toBeTruthy() return (await r.json()).access_token as string } console.log(`[nav] 429 retry ${i + 1}/2`) await new Promise((res) => setTimeout(res, 65000)) } throw new Error("Login retries exhausted") } /** * 核心页面导航冒烟:侧边栏主要入口能访问、文案库/配音库页面能正常加载(不出白屏/无致命 js error) */ test.describe("Core Navigation", () => { let authToken: string test.beforeAll(async ({ request }) => { const suffix = Math.random().toString(36).slice(2, 8) const email = `e2e-nav-${suffix}@example.com` await request.post(`${apiBase}/auth/register`, { data: { email, password: PASSWORD, username: `e2e_nav_${suffix}` }, }) authToken = await loginWithRetry(request, email, PASSWORD) const authHeader = { Authorization: `Bearer ${authToken}` } const proj = await request.post(`${apiBase}/projects`, { headers: authHeader, data: { name: `Smoke Nav ${suffix}` }, }) if (proj.ok()) { const projectId = (await proj.json()).id ?? (await proj.json()).project_id await request.post(`${apiBase}/asset-libraries`, { headers: authHeader, data: { project_id: projectId, name: "Nav Lib", kind: "video" }, }) } }) test.beforeEach(async ({ page }) => { await page.setViewportSize({ width: 1440, height: 900 }) await page.addInitScript((t: string) => { window.localStorage.setItem("access_token", t) window.localStorage.setItem( "auth-storage", JSON.stringify({ state: { token: t, user: null } }), ) }, authToken) await routeBrowserApiToTestApi(page) }) const navCases = [ { path: "/app/dashboard", marker: /概览|工作台|最近/i, name: "概览" }, { path: "/app/generate", marker: /智能剪辑|剪辑/, name: "智能剪辑" }, { path: "/app/assets", marker: /视频库|素材/, name: "视频库" }, { path: "/app/scripts", marker: /文案/, name: "文案库" }, { path: "/app/voices", marker: /配音|我的音色|配音库/, name: "配音库" }, { path: "/app/products", marker: /成品|作品/, name: "成品库" }, { path: "/app/history", marker: /历史|任务/, name: "任务历史" }, { path: "/app/tasks", marker: /任务中心|任务列表/, name: "任务中心" }, { path: "/app/points", marker: /积分|我的积分/, name: "积分中心" }, ] for (const c of navCases) { test(`visit ${c.name} (${c.path}) loads without fatal pageerror`, async ({ page }) => { const errors: Error[] = [] page.on("pageerror", (e) => errors.push(e)) await page.goto(c.path) await expect(page.locator("body")).not.toBeEmpty({ timeout: 20000 }) // 过滤掉常见第三方/非致命错误 const fatal = errors.filter( (e) => !/ResizeObserver|Loading chunk|network error|Failed to fetch|chunkLoadError/i.test( e.message, ), ) expect(fatal, `${c.name} pageerrors: ${fatal.map((e) => e.message).join("; ")}`).toHaveLength( 0, ) await expect( page.getByText(c.marker).first(), `${c.name} should show relevant text`, ).toBeVisible({ timeout: 15000 }) console.log(`[nav] ${c.name} loaded ✓`) }) } })