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 }); }); }; /** 登录操作,遇到 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 }, }); } type ProjectResponse = { id: string }; type LibraryResponse = { id: string }; test.describe("Core media upload flow", () => { test.describe.configure({ timeout: 180_000 }); test("uploads a video asset and shows it in the asset library", async ({ page, request, }) => { test.setTimeout(120_000); await routeBrowserApiToTestApi(page); const suffix = Date.now().toString(36); const email = `e2e-mov-${suffix}@example.com`; const username = `e2e_mov_${suffix}`; const register = await request.post(`${apiBase}/auth/register`, { data: { email, username, password: PASSWORD, display_name: username, }, }); expect(register.status(), await register.text()).toBe(201); const registerData = (await register.json()) as { user_id: string }; const login = await loginWithRetry(request, email, PASSWORD); expect(login.status(), await login.text()).toBe(200); const loginData = (await login.json()) as { access_token: string }; const headers = { Authorization: `Bearer ${loginData.access_token}` }; const project = await request.post(`${apiBase}/projects`, { headers, data: { name: `E2E Project ${suffix}`, description: "Playwright upload smoke", }, }); expect(project.status(), await project.text()).toBe(200); const projectData = (await project.json()) as ProjectResponse; const library = await request.post(`${apiBase}/asset-libraries`, { headers, data: { project_id: projectData.id, name: `E2E Video Library ${suffix}`, kind: "video", }, }); expect(library.status(), await library.text()).toBe(200); const libraryData = (await library.json()) as LibraryResponse; await page.addInitScript( ({ token, user }) => { localStorage.setItem("access_token", token); localStorage.setItem( "auth-storage", JSON.stringify({ state: { user, isAuthenticated: true }, version: 0, }), ); }, { token: loginData.access_token, user: { id: registerData.user_id, user_id: registerData.user_id, email, username, display_name: username, is_email_verified: true, email_verified: true, }, }, ); await page.goto("/app/assets"); await expect(page.locator(".xx-assets-layout")).toBeVisible({ timeout: 20_000, }); const upload = await request.post(`${apiBase}/upload`, { headers, multipart: { project_id: projectData.id, library_id: libraryData.id, file: { name: "e2e-sample.MOV", mimeType: "video/quicktime", buffer: Buffer.from("playwright mov upload smoke"), }, }, }); expect(upload.status(), await upload.text()).toBe(200); await expect( page.getByText(/上传失败|素材列表加载失败|素材库加载失败/), ).toHaveCount(0, { timeout: 5_000 }); await expect .poll( async () => { const assets = await request.get(`${apiBase}/assets`, { headers, params: { library_id: libraryData.id }, }); if (!assets.ok()) { return `http_${assets.status()}`; } const data = (await assets.json()) as { items: Array<{ name: string; status: string; file_type?: string; mime_type?: string; }>; }; const asset = data.items.find( (item) => item.name === "e2e-sample.MOV", ); return asset ? `${asset.mime_type || asset.file_type || ""}:${asset.status}` : "missing"; }, { timeout: 30_000, intervals: [1_000, 2_000, 3_000] }, ) .toMatch(/^(video\/quicktime|video\/mp4|video)?:ready$/); // Select the test library from sidebar await page .locator(".xx-asset-library-item") .filter({ hasText: `E2E Video Library ${suffix}` }) .click({ force: true }); await page.reload(); await expect(page.locator(".xx-assets-content")).toBeVisible({ timeout: 20_000, }); await expect(page.getByText("e2e-sample.MOV", { exact: true })).toBeVisible({ timeout: 20_000, }); // Verify asset card shows status const assetCard = page .locator(".xx-asset-card") .filter({ hasText: "e2e-sample.MOV" }); await expect(assetCard).toBeVisible(); await expect(assetCard.locator(".xx-asset-diagnose-btn")).toBeVisible(); await expect(page.getByText(/素材加载失败|上传失败/)).toHaveCount(0); }); });