120 lines
7.3 KiB
TypeScript
120 lines
7.3 KiB
TypeScript
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);
|
||
});
|