import { expect, test, type Page } from "@playwright/test"; test.setTimeout(30_000); const browserErrors = new WeakMap(); 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: "" }] }) }, ); 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: '' }); }); 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); });