8935196fcd
Deploy / Staging E2E Tests (push) Has been skipped
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
Deploy / Deploy Staging (push) Failing after 138h4m33s
CI/CD Pipeline / Frontend Lint (push) Failing after 138h4m39s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 138h4m39s
348 lines
12 KiB
TypeScript
Executable File
348 lines
12 KiB
TypeScript
Executable File
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 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 project = await request.post(`${apiBase}/projects`, {
|
|
headers,
|
|
data: { 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: { 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: { 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: {
|
|
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 }) => {
|
|
localStorage.setItem("access_token", token);
|
|
localStorage.setItem(
|
|
"auth-storage",
|
|
JSON.stringify({
|
|
state: { user, isAuthenticated: true },
|
|
version: 0,
|
|
}),
|
|
);
|
|
},
|
|
{
|
|
token: loginData.access_token,
|
|
projectId: projectData.id,
|
|
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,
|
|
});
|
|
});
|
|
});
|