import { expect, test, type APIRequestContext } from "@playwright/test" import * as fs from "node:fs" import * as path from "node:path" import { fileURLToPath } from "node:url" const __dirname = path.dirname(fileURLToPath(import.meta.url)) 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 sampleVideoPath = path.join(__dirname, "fixtures", "sample.mp4") const sampleVideoBuffer = fs.readFileSync(sampleVideoPath) const upload = await request.post(`${apiBase}/upload`, { headers, multipart: { project_id: projectData.id, library_id: libraryData.id, file: { name: "e2e-sample.mp4", mimeType: "video/mp4", buffer: sampleVideoBuffer, }, }, }) 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.mp4") 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.mp4", { exact: true })).toBeVisible({ timeout: 20_000, }) // Verify asset card shows status const assetCard = page.locator(".xx-asset-card").filter({ hasText: "e2e-sample.mp4" }) await expect(assetCard).toBeVisible() await expect(assetCard.locator(".xx-asset-diagnose-btn")).toBeVisible() await expect(page.getByText(/素材加载失败|上传失败/)).toHaveCount(0) }) })