diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index ee6f8fa81..844d5e868 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -226,4 +226,4 @@ jobs: -v "$PWD:/workspace" \ -w /workspace/apps/web \ mcr.microsoft.com/playwright:v1.45.0-jammy \ - sh -lc 'npm ci && npx playwright test --project=chromium e2e/core-upload.spec.ts' + sh -lc 'npm ci && npx playwright test --project=chromium e2e/core-upload.spec.ts e2e/core-generation.spec.ts' diff --git a/apps/web/e2e/core-generation.spec.ts b/apps/web/e2e/core-generation.spec.ts new file mode 100644 index 000000000..18d8466e1 --- /dev/null +++ b/apps/web/e2e/core-generation.spec.ts @@ -0,0 +1,112 @@ +import { expect, test } from '@playwright/test'; + +const PASSWORD = 'SmokePass123!'; + +type WorkspaceResponse = { id?: string; workspace_id?: string }; +type ProjectResponse = { id: string }; +type LibraryResponse = { id: string }; +type GenerationTaskResponse = { id: string; status: string; progress: number; result_count: number; error_message?: string | null }; +type GeneratedVideoResponse = { id: string; name: string; file_url: string; file_size: number }; + +test.describe('Core generation and download flow', () => { + test('generates an MP4 from the browser and exposes a playable download', async ({ page, request }) => { + test.setTimeout(180_000); + + const suffix = Date.now().toString(36); + const email = `e2e-generation-${suffix}@example.com`; + const username = `e2e_generation_${suffix}`; + const libraryName = `E2E Generation Library ${suffix}`; + const apiBase = '/api/v1'; + + 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 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 Generation 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 Generation Project ${suffix}` }, + }); + 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: libraryName, kind: 'video' }, + }); + expect(library.status(), await library.text()).toBe(200); + const libraryData = (await library.json()) as LibraryResponse; + + await page.goto('/login'); + await page.getByPlaceholder('邮箱').fill(email); + await page.getByPlaceholder('密码').fill(PASSWORD); + await page.getByRole('button', { name: /登\s*录/ }).click(); + await expect(page).toHaveURL(/\/workspaces/, { timeout: 20_000 }); + + await page.goto(`/projects/${projectData.id}/generation`); + await expect(page.getByText('项目生成任务')).toBeVisible({ timeout: 20_000 }); + await page.locator('.ant-select-selector').first().click(); + await page.getByText(`${libraryName} (video)`).click(); + + const createTaskResponsePromise = page.waitForResponse( + (response) => response.url().includes('/api/v1/generation/tasks') && response.request().method() === 'POST', + { timeout: 30_000 } + ); + await page.getByRole('button', { name: '发起生成' }).click(); + const createTaskResponse = await createTaskResponsePromise; + expect(createTaskResponse.status(), await createTaskResponse.text()).toBe(200); + const createdTask = (await createTaskResponse.json()) as GenerationTaskResponse; + + await expect(page.getByText(/任务状态:生成完成/)).toBeVisible({ timeout: 90_000 }); + await expect(page.getByText(/生成失败|生成任务查询失败|生成结果查询失败/)).toHaveCount(0); + + await expect + .poll( + async () => { + const task = await request.get(`${apiBase}/generation/tasks/${createdTask.id}`, { headers }); + if (!task.ok()) { + return `http_${task.status()}`; + } + const data = (await task.json()) as GenerationTaskResponse; + return `${data.status}:${data.result_count}:${data.error_message || ''}`; + }, + { timeout: 90_000, intervals: [1_000, 2_000, 5_000] } + ) + .toMatch(/^completed:[1-9]\d*:/); + + const results = await request.get(`${apiBase}/generation/tasks/${createdTask.id}/results`, { headers }); + expect(results.status(), await results.text()).toBe(200); + const resultsData = (await results.json()) as { items: GeneratedVideoResponse[] }; + expect(resultsData.items.length).toBeGreaterThan(0); + const generatedVideo = resultsData.items[0]; + expect(generatedVideo.name).toMatch(/\.mp4$/); + expect(generatedVideo.file_size).toBeGreaterThan(0); + + await page.goto(`/projects/${projectData.id}/results`); + await expect(page.getByText(generatedVideo.name, { exact: true })).toBeVisible({ timeout: 20_000 }); + const downloadUrlResponse = await request.get(`${apiBase}/generated-videos/${generatedVideo.id}/download-url`, { headers }); + expect(downloadUrlResponse.status(), await downloadUrlResponse.text()).toBe(200); + const downloadData = (await downloadUrlResponse.json()) as { download_url: string }; + const videoResponse = await request.get(downloadData.download_url, { timeout: 30_000 }); + expect(videoResponse.status(), await videoResponse.text()).toBe(200); + expect(videoResponse.headers()['content-type'] || '').toContain('video/mp4'); + const videoBody = await videoResponse.body(); + expect(videoBody.length).toBeGreaterThan(1024); + }); +});