Files
xiaoxia-saas/apps/web/e2e/core-generation.spec.ts
T
Xiaoxia AI dcd51f48de
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 25s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Successful in 25s
Tests / test (pull_request) Successful in 17s
Deploy / Deploy Staging (push) Successful in 1m20s
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
Tests / lint (pull_request) Successful in 12s
test(web): target generation approval pill
2026-06-25 12:58:36 +08:00

236 lines
12 KiB
TypeScript

import { fileURLToPath } from 'node:url';
import { expect, test } from '@playwright/test';
import fs from 'node:fs';
import path from 'node:path';
const currentDir = 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 });
});
};
type WorkspaceResponse = { id?: string; workspace_id?: string };
type ProjectResponse = { id: string };
type LibraryResponse = { id: string };
type AssetListResponse = { items: Array<{ name: string; status: string; mime_type?: string; file_type?: string }> };
type GenerationTaskResponse = { id: string; status: string; progress: number; result_count: number; error_message?: string | null; strategy_id?: string | null; edit_plan_id?: string | null };
type ProjectTitleResponse = { id: string; text: string; usage_count: number };
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);
await routeBrowserApiToTestApi(page);
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 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 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;
const projectTitleText = `E2E 生成标题 ${suffix}`;
const title = await request.post(`${apiBase}/projects/${projectData.id}/titles`, {
headers,
data: { workspace_id: workspaceId, text: projectTitleText, category: 'marketing', favorite: true },
});
expect(title.status(), await title.text()).toBe(200);
const titleData = (await title.json()) as ProjectTitleResponse;
const fixture = fs.readFileSync(path.join(currentDir, 'fixtures', 'sample.mp4'));
const upload = await request.post(`${apiBase}/upload`, {
headers,
multipart: {
workspace_id: workspaceId || '',
project_id: projectData.id,
library_id: libraryData.id,
file: {
name: 'e2e-generation-source.mp4',
mimeType: 'video/mp4',
buffer: fixture,
},
},
});
expect(upload.status(), await upload.text()).toBe(200);
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 AssetListResponse;
const asset = data.items.find((item) => item.name === 'e2e-generation-source.mp4');
return asset ? `${asset.mime_type || asset.file_type || ''}:${asset.status}` : 'missing';
},
{ timeout: 90_000, intervals: [1_000, 2_000, 3_000, 5_000] }
)
.toMatch(/^(video\/quicktime|video\/mp4|video)?:ready$/);
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}/generation`);
await expect(page.getByText('剪辑参数')).toBeVisible({ timeout: 20_000 });
await page.locator('.ant-select-selector').first().click();
await page.getByText(`${libraryName} (video)`).click();
await page.locator('.ant-select-selector').nth(1).click();
await page.getByText(projectTitleText).click();
await expect(page.getByText(/素材就绪度:/)).toBeVisible({ timeout: 20_000 });
await expect(page.getByRole('button', { name: '重新生成计划' })).toBeEnabled({ timeout: 20_000 });
await page.getByRole('button', { name: '重新生成计划' }).click();
await expect(page.getByText('剪辑计划预览')).toBeVisible({ timeout: 20_000 });
await expect(page.getByText(/自动选择/)).toBeVisible({ timeout: 20_000 });
await expect(page.getByText(/e2e-generation-source\.mp4/)).toBeVisible({ timeout: 20_000 });
const confirmGenerationButton = page.getByRole('complementary').getByRole('button', { name: '确认计划并生成' });
await expect(confirmGenerationButton).toBeEnabled({ timeout: 20_000 });
const createTaskResponsePromise = page.waitForResponse(
(response) => response.url().includes('/api/v1/generation/tasks') && response.request().method() === 'POST',
{ timeout: 30_000 }
);
await confirmGenerationButton.click();
const createTaskResponse = await createTaskResponsePromise;
expect(createTaskResponse.status(), await createTaskResponse.text()).toBe(200);
const createdTask = (await createTaskResponse.json()) as GenerationTaskResponse;
expect(createdTask.edit_plan_id || '').not.toBe('');
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.strategy_id || ''}:${data.error_message || ''}`;
},
{ timeout: 90_000, intervals: [1_000, 2_000, 5_000] }
)
.toMatch(new RegExp(`^completed:[1-9]\\d*:${titleData.id}:`));
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`);
const resultCard = page.locator('.xx-vertical-card').filter({ hasText: generatedVideo.name });
await expect(resultCard).toBeVisible({ timeout: 20_000 });
await expect(resultCard.getByText('待复核')).toBeVisible({ timeout: 20_000 });
await expect(resultCard.getByRole('button', { name: /下载/ })).toBeVisible();
await expect(page.getByRole('button', { name: '批量获取下载地址' })).toBeEnabled();
await resultCard.getByRole('button', { name: '可发布' }).click();
await expect(page.getByText('成片复核状态已更新')).toBeVisible({ timeout: 10_000 });
await expect(resultCard.locator('.xx-pill.ok', { hasText: '可发布' })).toBeVisible({ timeout: 20_000 });
const reviewedVideo = await request.get(`${apiBase}/generated-videos/${generatedVideo.id}`, { headers });
expect(reviewedVideo.status(), await reviewedVideo.text()).toBe(200);
const reviewedVideoData = (await reviewedVideo.json()) as { review_status: string; generation_params: Record<string, unknown> };
expect(reviewedVideoData.review_status).toBe('approved');
expect(reviewedVideoData.generation_params.title_id).toBe(titleData.id);
expect(reviewedVideoData.generation_params.edit_plan_id).toBe(createdTask.edit_plan_id);
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);
const assetsAfterGeneration = await request.get(`${apiBase}/assets`, {
headers,
params: { library_id: libraryData.id },
});
expect(assetsAfterGeneration.status(), await assetsAfterGeneration.text()).toBe(200);
const assetsAfterGenerationData = (await assetsAfterGeneration.json()) as { items: Array<{ name: string; metadata: Record<string, unknown> }> };
const sourceAsset = assetsAfterGenerationData.items.find((item) => item.name === 'e2e-generation-source.mp4');
expect(sourceAsset?.metadata.generation_use_count).toBe(1);
expect(sourceAsset?.metadata.review_status).toBe('pending_review');
const titleAfterGeneration = await request.get(`${apiBase}/projects/${projectData.id}/titles`, { headers });
expect(titleAfterGeneration.status(), await titleAfterGeneration.text()).toBe(200);
const titlesData = (await titleAfterGeneration.json()) as { items: ProjectTitleResponse[] };
expect(titlesData.items.find((item) => item.id === titleData.id)?.usage_count).toBe(1);
await page.goto(`/projects/${projectData.id}/tasks`);
await expect(page.getByText('项目任务中心')).toBeVisible({ timeout: 20_000 });
await expect(page.getByText('视频生成')).toBeVisible({ timeout: 20_000 });
await expect(page.getByText('已完成').first()).toBeVisible({ timeout: 20_000 });
await expect(page.getByText(createdTask.id)).toBeVisible({ timeout: 20_000 });
});
});