ai-chatbot/tests/e2e/canvas-generation-feedback.test.ts

120 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, test, type Page } from "@playwright/test";
test.setTimeout(30_000);
const browserErrors = new WeakMap<Page, string[]>();
test.beforeEach(({ page }) => {
const errors: string[] = [];
browserErrors.set(page, errors);
page.on("pageerror", error => errors.push(error.message));
});
test.afterEach(({ page }) => expect(browserErrors.get(page)).toEqual([]));
const projectId = "24cce650-7cfb-48bb-aa65-b04f490ac978";
const imageOrigin = "https://scqnycdn.jiansun.vip";
const failure = {
phase: "failed", requestId: "failed-request", userMessageId: "",
failure: { code: "SOURCE_IMAGE_UNAVAILABLE", outcome: "failed", message: "参考图片无法读取。", recommendation: "请重新上传原图后再提交。" },
};
async function setup(page: Page, lostStream = false) {
const submissions: { path: string; body: any }[] = [];
let messages: any[] = [];
const canvasData = {
nodes: [{ id: "preview-image", type: "image", position: { x: 220, y: 160 }, size: { width: 640, height: 420 },
data: { status: "completed", src: `${imageOrigin}/uploads/feedback-preview.png`, title: "参考图片" } }],
edges: [], viewport: { x: 0, y: 0, zoom: 1 },
};
await page.context().addInitScript(() => localStorage.setItem("theme", "light"));
await page.context().route("**/api/**", async route => {
const request = route.request();
const path = new URL(request.url()).pathname;
let data: unknown = {};
if (["/api/chat", "/api/canvas/agent-chat"].includes(path) && request.method() === "POST") {
const body = request.postDataJSON();
submissions.push({ path, body });
const terminal = { ...failure, userMessageId: body.message.id };
const text = `${terminal.failure.message}${terminal.failure.recommendation}\n\n请求编号:${terminal.requestId}`;
const assistant = { id: "failed-assistant", role: "assistant", createdAt: new Date(Date.now() + 100).toISOString(),
parts: [{ type: "text", text }, { type: "data-imageGenerationStatus", data: terminal }] };
messages = [{ ...body.message, createdAt: new Date().toISOString() }, assistant];
const chunks: any[] = [{ type: "start", messageId: "stream-assistant" },
{ type: "data-imageGenerationStatus", data: JSON.stringify({ phase: "processing", userMessageId: body.message.id }) }];
if (!lostStream) chunks.push(
{ type: "data-imageGenerationStatus", data: JSON.stringify(terminal) },
{ type: "data-appendMessage", data: JSON.stringify(assistant) },
{ type: "data-appendMessage", data: JSON.stringify({ id: "end", role: "assistant", parts: [{ type: "text", text: "<!-- IMAGE_GENERATION_END -->" }] }) },
);
chunks.push({ type: "finish" });
await route.fulfill({ headers: { "content-type": "text/event-stream", "x-vercel-ai-ui-message-stream": "v1" },
body: `${chunks.map(chunk => `data: ${JSON.stringify(chunk)}\n\n`).join("")}data: [DONE]\n\n` });
return;
}
if (path.includes("validate-session")) data = { valid: true, success: true };
else if (path === "/api/auth/session") data = { user: { id: "feedback-user", name: "测试", type: "admin" }, expires: "2099-01-01" };
else if (path === "/api/site-settings") data = { canvasEnabled: true, agentEnabled: true, membershipEnabled: false };
else if (path === "/api/vote") data = [];
else if (path === "/api/models") data = { models: [{ id: "image-test", modelId: "gpt-image-2", providerType: "openai", name: "GPT Image", type: "image", enabled: true, supportsImageGen: true, imageGenConfig: { creditCostPerImage: 15 } }] };
else if (path === `/api/canvas/${projectId}` && request.method() === "GET") data = { success: true, data: { id: projectId, title: "反馈验收", canvasData } };
else if (path.includes("messages")) data = { messages };
else if (path.includes("sessions")) data = { sessions: [] };
else if (path.startsWith("/api/canvas/")) data = { success: true, runs: [] };
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(data) });
});
await page.context().route(`${imageOrigin}/**`, async route => {
await new Promise(resolve => setTimeout(resolve, 200));
await route.fulfill({ contentType: "image/svg+xml", body: '<svg xmlns="http://www.w3.org/2000/svg" width="640" height="420"><rect width="640" height="420" fill="#43b9bd"/></svg>' });
});
await page.goto(`/canvas/${projectId}`, { waitUntil: "domcontentloaded" });
await expect(page.getByRole("button", { name: "关闭 Agent,直接使用生图模型", exact: true })).toBeVisible();
return submissions;
}
test("image workspaces start with Agent enabled and send requests to its planning runtime", async ({ page }) => {
const submissions = await setup(page);
await page.getByLabel("描述你的设计需求", { exact: true }).fill("请先分析参考图片,再规划处理步骤。");
await page.getByLabel("描述你的设计需求", { exact: true }).press("Enter");
await expect.poll(() => submissions.length).toBe(1);
expect(submissions[0].path).toBe("/api/canvas/agent-chat");
expect(submissions[0].body.composerMode).toBe("agent");
});
for (const lostStream of [false, true]) {
test(`image failure ends waiting and shows its cause${lostStream ? " after recovering history" : " from the stream"}`, async ({ page }) => {
const submissions = await setup(page, lostStream);
await page.getByRole("button", { name: "关闭 Agent,直接使用生图模型", exact: true }).click();
await page.getByLabel("描述你的设计需求", { exact: true }).fill("将参考图改成白色背景。");
await page.getByLabel("描述你的设计需求", { exact: true }).press("Enter");
const failedNode = page.locator('[data-canvas-node-id] [role="alert"]');
await expect(failedNode).toContainText("参考图片无法读取。");
await expect(failedNode).toContainText("请重新上传原图后再提交。");
await expect(failedNode).toContainText("请求编号:failed-request");
await expect(page.getByTestId("message-assistant-image-loading")).toHaveCount(0);
expect(submissions).toHaveLength(1);
expect(submissions[0].path).toBe("/api/chat");
await page.screenshot({ path: test.info().outputPath("terminal-failure.png") });
});
}
test("zoom and hover keep a loaded canvas image visible while a new preview loads", async ({ page }) => {
await setup(page);
const node = page.locator('[data-canvas-node-id="preview-image"]');
await expect.poll(() => node.locator("img").first().evaluate((image: HTMLImageElement) => image.naturalWidth)).toBeGreaterThan(0);
await page.evaluate(() => {
(window as any).blankPreviewFrames = 0;
const inspect = () => {
const node = document.querySelector('[data-canvas-node-id="preview-image"]');
if (node && !Array.from(node.querySelectorAll("img")).some(image => Number(getComputedStyle(image).opacity) > 0 && image.naturalWidth > 0)) (window as any).blankPreviewFrames++;
requestAnimationFrame(inspect);
};
requestAnimationFrame(inspect);
});
await page.mouse.move(500, 350);
for (let i = 0; i < 18; i++) {
await page.mouse.wheel(0, i < 9 ? -100 : 100);
await page.waitForTimeout(50);
}
await page.mouse.move(100, 100);
await page.waitForTimeout(500);
expect(await page.evaluate(() => (window as any).blankPreviewFrames)).toBe(0);
});