import { expect, test, type Locator } from "@playwright/test"; import { createServer } from "node:http"; import type { AddressInfo } from "node:net"; import { mockAgentApi } from "./support/mock-agent-api"; import { mockWorkbenchContext } from "./support/mock-workbench-context"; import { responseFixtureWriter } from "./support/responses-fixture"; async function readRowStyle(row: Locator) { return row.evaluate((element) => { const style = getComputedStyle(element); const text = element.querySelector("span.min-w-0 > span")!; const icon = element.querySelector('[role="img"]')!; return { columns: style.gridTemplateColumns.split(" ")[0], gap: style.columnGap, fontSize: style.fontSize, titleWeight: getComputedStyle(text).fontWeight, titleLineHeight: getComputedStyle(text).lineHeight, iconWidth: icon.getBoundingClientRect().width, iconHeight: icon.getBoundingClientRect().height }; }); } for (const width of [1440, 320]) { test(`live progress matches archived rows and retains folding at ${width}px`, async ({ page }, testInfo) => { await page.setViewportSize({ width, height: 900 }); await page.emulateMedia({ reducedMotion: "reduce" }); await mockWorkbenchContext(page); await mockAgentApi(page); const steps = [ { id: "start", title: "已收到请求", status: "completed", detail: "正在核对当前项目。" }, { id: "context", title: "Agent 正在处理请求", status: "running", detail: "Agent 正在执行推理、工具调用或结果整理。" }, { id: "data", title: "获取业务数据", status: "running", detail: "正在核对附近阀门、关联管道与隔离范围。" }, { id: "isolation", phase: "tool", title: "阀门隔离分析", status: "running", tool: { name: "mcp_tjwater_analyze_valve_isolation", call_id: "isolation-call", input: { link_ids: [533793, 486121, 523616] } } } ]; let finish!: () => void; const server = createServer((_request, response) => { response.writeHead(200, { "Access-Control-Allow-Origin": "*", "Content-Type": "text/event-stream" }); const write = responseFixtureWriter(); const send = (chunk: Record) => response.write(write(chunk)); send({ type: "start", messageId: "consistent-progress" }); for (const step of steps) send({ type: "data-progress", data: step }); finish = () => { for (const step of steps) send({ type: "data-progress", data: { ...step, status: "completed" } }); send({ type: "data-final_answer", data: { content: "已核对附近可用于隔离的阀门。" } }); send({ type: "finish", finishReason: "stop" }); response.end(); }; }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { await page.route("**/api/v1/responses", (route) => route.continue({ url: `http://127.0.0.1:${(server.address() as AddressInfo).port}` })); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); await panel.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }).fill("找下附近可用的阀门,用于隔离"); await panel.getByRole("button", { name: "发送 Agent 指令", exact: true }).click(); const message = panel.locator('[data-agent-message-id="consistent-progress"]'); const progress = message.locator("[data-agent-progress]"); const recent = progress.getByRole("list", { name: "最近执行进度" }); await expect(recent.locator("li")).toHaveCount(3); const container = message.locator("details[data-agent-live-process]"); await expect(container).toHaveCSS("background-color", "rgb(255, 255, 255)"); await expect(container).toHaveCSS("border-radius", "12px"); await expect(container).toHaveCSS("border-top-width", "1px"); await expect(progress).toHaveCSS("border-top-width", "0px"); const containerStyle = await container.evaluate((element) => { const style = getComputedStyle(element); return [style.borderColor, style.borderRadius, style.backgroundColor]; }); const liveRow = recent.locator("li").filter({ hasText: "Agent 正在处理请求" }); const liveStyle = await readRowStyle(liveRow); expect(liveStyle).toMatchObject({ columns: "24px", gap: "8px", fontSize: "14px", titleWeight: "500", iconWidth: 24, iconHeight: 24 }); await expect(liveRow.getByRole("img", { name: "进行中", exact: true })).toBeVisible(); await progress.getByRole("button", { name: "展开执行进度", exact: true }).click(); await expect(progress.getByRole("list", { name: "全部执行进度" }).locator("li")).toHaveCount(4); await progress.getByRole("button", { name: "收起执行进度", exact: true }).click(); await expect(recent.locator("li")).toHaveCount(3); const tool = progress.locator("details[data-agent-tool]"); await expect(tool).not.toHaveAttribute("open"); await tool.locator("summary").click(); await expect(tool.locator("pre")).toContainText("533793"); await message.screenshot({ path: testInfo.outputPath(`live-progress-${width}.png`) }); finish(); const archived = message.locator("details[data-agent-process]"); await expect(archived).not.toHaveAttribute("open"); await message.getByRole("button", { name: "查看处理过程", exact: true }).click(); await expect(archived).toHaveAttribute("open", ""); await expect(archived).toHaveCSS("background-color", "rgb(255, 255, 255)"); expect(await archived.evaluate((element) => { const style = getComputedStyle(element); return [style.borderColor, style.borderRadius, style.backgroundColor]; })).toEqual(containerStyle); const archivedRow = archived.getByRole("list", { name: "全部执行进度" }).locator("li").filter({ hasText: "Agent 正在处理请求" }); expect(await readRowStyle(archivedRow)).toEqual(liveStyle); await expect(archivedRow.getByRole("img", { name: "已完成", exact: true })).toBeVisible(); await expect(tool).toHaveAttribute("open", ""); await message.getByRole("button", { name: "查看处理过程", exact: true }).click(); await expect(progress).toBeHidden(); await message.getByRole("button", { name: "查看处理过程", exact: true }).click(); await expect(progress).toBeVisible(); await message.screenshot({ path: testInfo.outputPath(`archived-progress-${width}.png`) }); expect(await progress.evaluate((element) => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1); } finally { server.closeAllConnections(); await new Promise((resolve) => server.close(() => resolve())); } }); }