import { expect, test, type Page } from "@playwright/test"; import { createServer } from "node:http"; import type { AddressInfo } from "node:net"; import { responseFixtureWriter } from "./support/responses-fixture"; import { mockAgentApi } from "./support/mock-agent-api"; import { mockWorkbenchContext } from "./support/mock-workbench-context"; const finalText = "已完成三根管道的压力核查。结果属于模型计算,仍需现场核验。"; async function openStream(page: Page, width: number) { await page.setViewportSize({ width, height: 900 }); await mockWorkbenchContext(page); await mockAgentApi(page); let publishFinal!: () => void; let finish!: () => void; const server = createServer((_request, response) => { response.writeHead(200, { "Access-Control-Allow-Origin": "*", "Content-Type": "text/event-stream" }); const encode = responseFixtureWriter(); const send = (chunk: Record) => response.write(encode(chunk)); send({ type: "start", messageId: "answer-transition" }); send({ type: "text-start", id: "process-text" }); send({ type: "text-delta", id: "process-text", delta: Array.from({ length: 8 }, (_, i) => `第${i + 1}阶段:正在核对管道压力与模拟时段,本阶段数据仅用于分析过程。`).join("\n\n") }); for (let i = 0; i < 6; i++) send({ type: "data-progress", data: { id: `step-${i}`, phase: "tool", title: `核对资料步骤 ${i + 1}`, detail: "查询监测记录并检查时段与单位。", status: i === 5 ? "running" : "completed" } }); send({ type: "text-end", id: "process-text" }); publishFinal = () => send({ type: "data-final_answer", data: { content: finalText, ui_block_ids: [] } }); finish = () => { send({ type: "finish", finishReason: "stop" }); response.end(); }; }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); await page.route("**/api/v1/responses", (route) => route.continue({ url: `http://127.0.0.1:${(server.address() as AddressInfo).port}/stream` })); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); await panel.locator("textarea").fill("核对压力"); await panel.getByRole("button", { name: "发送 Agent 指令", exact: true }).click(); const message = panel.locator('[data-agent-message-id="answer-transition"]'); await expect(message.getByText("核对资料步骤 6", { exact: true })).toBeVisible(); return { panel, message, publishFinal: () => publishFinal(), finish: () => finish(), close: async () => { server.closeAllConnections(); await new Promise((resolve) => server.close(() => resolve())); } }; } for (const width of [1440, 320]) { for (const reducedMotion of ["reduce", "no-preference"] as const) { test(`final report settles once before transport close at ${width}px with ${reducedMotion}`, async ({ page }, testInfo) => { await page.emulateMedia({ reducedMotion }); const stream = await openStream(page, width); try { const { message } = stream; await message.locator("[data-agent-progress]").evaluate((node) => { node.setAttribute("data-original-progress", "true"); }); stream.publishFinal(); await expect(message.getByText(finalText, { exact: true })).toBeVisible(); await expect(message.getByRole("button", { name: "复制回复", exact: true })).toBeVisible(); await expect(message.getByText("正在回复", { exact: true })).toHaveCount(0); await expect(message.locator("details[data-agent-process]")).not.toHaveAttribute("open"); const settled = await message.boundingBox(); await message.screenshot({ path: testInfo.outputPath(`report-ready-${width}-${reducedMotion}.png`) }); stream.finish(); await expect(stream.panel.getByRole("button", { name: "发送 Agent 指令", exact: true })).toBeVisible(); const after = await message.boundingBox(); expect(Math.abs(after!.height - settled!.height)).toBeLessThanOrEqual(1); expect(Math.abs(after!.y - settled!.y)).toBeLessThanOrEqual(1); await message.getByRole("button", { name: "查看处理过程", exact: true }).click(); const process = message.locator("details[data-agent-process]"); await expect(process.getByText(/第1阶段:/)).toBeVisible(); await expect(process.locator('[data-original-progress="true"]')).toBeVisible(); } finally { await stream.close(); } }); } } for (const reducedMotion of ["reduce", "no-preference"] as const) test(`reading a live process still folds on completion and retains tool details with ${reducedMotion}`, async ({ page }) => { await page.emulateMedia({ reducedMotion }); const stream = await openStream(page, 1440); try { const { message, panel } = stream; const tool = message.locator("details[data-agent-tool]").last(); await tool.locator("summary").click(); const detail = tool.getByText("查询监测记录并检查时段与单位。", { exact: true }); await expect(detail).toBeVisible(); await expect(tool).toHaveAttribute("data-disclosure-motion", "idle"); const scroll = panel.locator(".agent-conversation-scroll"); await scroll.hover(); await page.mouse.wheel(0, -300); await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))); const reading = message.getByText(/第1阶段:/).first(); await expect(reading).toBeInViewport(); // Finish the reader's wheel movement and any preceding disclosure layout // before measuring the separate final-report handoff. let lastTop: number | undefined; await expect.poll(async () => { const top = (await reading.boundingBox())!.y; const stable = top === lastTop; lastTop = top; return stable; }).toBe(true); stream.publishFinal(); await expect(message.getByRole("button", { name: "复制回复", exact: true })).toBeVisible(); const process = message.locator("details[data-agent-process]"); await expect(process).not.toHaveAttribute("open"); await expect(detail).toBeHidden(); await expect(message.locator("[data-answer-phase]")).toHaveAttribute("data-answer-phase", "settled"); await expect(message.getByText(finalText, { exact: true })).toBeInViewport(); stream.finish(); await expect(panel.getByRole("button", { name: "发送 Agent 指令", exact: true })).toBeVisible(); await expect(process).not.toHaveAttribute("open"); await message.getByRole("button", { name: "查看处理过程", exact: true }).click(); await expect(process).toHaveAttribute("open", ""); await expect(process.locator("details[data-agent-tool]").last()).toHaveAttribute("open", ""); await expect(detail).toBeVisible(); await message.getByRole("button", { name: "查看处理过程", exact: true }).click(); await expect(process).not.toHaveAttribute("open"); } finally { await stream.close(); } });