import { expect, test } 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 }); }); }; type WorkspaceResponse = { id?: string; workspace_id?: string }; type ProjectResponse = { id: string }; type LibraryResponse = { id: string }; test.describe('Core media upload flow', () => { test('uploads a MOV asset from the browser and shows it as ready', 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 request.post(`${apiBase}/auth/login`, { data: { email, password: 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 workspace = await request.post(`${apiBase}/workspaces`, { headers, data: { name: `E2E Workspace ${suffix}` }, }); expect(workspace.status(), await workspace.text()).toBe(201); const workspaceData = (await workspace.json()) as WorkspaceResponse; const workspaceId = workspaceData.id || workspaceData.workspace_id; expect(workspaceId).toBeTruthy(); const project = await request.post(`${apiBase}/projects`, { headers, data: { workspace_id: workspaceId, 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: { workspace_id: workspaceId, 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, projectId, workspaceId }) => { localStorage.setItem('access_token', token); localStorage.setItem('auth-storage', JSON.stringify({ state: { user, isAuthenticated: true }, version: 0 })); sessionStorage.setItem(`project-workspace:${projectId}`, workspaceId); }, { token: loginData.access_token, projectId: projectData.id, workspaceId, 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(`/projects/${projectData.id}/assets`); await expect(page.getByText('点击或拖拽素材到这里上传')).toBeEnabled({ timeout: 20_000 }); const upload = await request.post(`${apiBase}/upload`, { headers, multipart: { workspace_id: workspaceId || '', 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$/); await page.reload(); await expect(page.getByText(/素材就绪度|Ready/)).toBeVisible({ timeout: 20_000 }); await expect(page.getByText(/预计成片|视频素材数量偏少|素材准备度良好/)).toBeVisible({ timeout: 20_000 }); await expect(page.getByText('e2e-sample.MOV', { exact: true })).toBeVisible({ timeout: 20_000 }); await page.locator('.xx-vertical-card').filter({ hasText: 'e2e-sample.MOV' }).getByRole('button', { name: /通\s*过/ }).click(); await expect(page.getByText('复核状态已更新')).toBeVisible({ timeout: 10_000 }); await expect(page.getByText(/已通过|approved/)).toBeVisible({ timeout: 20_000 }); await expect(page.getByText(/素材加载失败|上传失败/)).toHaveCount(0); }); });