import { expect, test } from "@playwright/test"; import { mockAgentApi } from "./support/mock-agent-api"; import { mockWorkbenchContext } from "./support/mock-workbench-context"; import type { AnswerViewStateRecord, AnswerViewStateWrite } from "../../src/features/agent/api/answer-view-state"; for (const width of [1440, 375, 320]) { test(`final UI anchors isolate process content and restore final controls at ${width}px`, async ({ page, context }) => { test.setTimeout(60000); await page.setViewportSize({ width, height: 900 }); await page.emulateMedia({ reducedMotion: "reduce" }); await mockWorkbenchContext(page, undefined, undefined, {}, [], { runtimeMode: "backend" }); const sessionId = "anchored-answer", title = "最终界面定位"; await mockAgentApi(page, { sessions: [{ session_id: sessionId, title, created_at: Date.now(), updated_at: Date.now() }] }); const process = { version: "answer-ui@1", block_id: "query-process", phase: "process", fallback_text: "中间查询资料,仅过程使用。", spec: { root: "note", elements: { note: { type: "Text", props: { text: "中间查询资料,仅过程使用。", tone: "neutral" }, children: [] } } }, }; const table = { version: "answer-ui@1", block_id: "report-table", phase: "final", fallback_text: "最终读数:北侧 0.30 MPa,南侧 0.28 MPa。", spec: { root: "table", elements: { table: { type: "Table", props: { title: "最终读数", columns: ["位置", "压力"], rows: [["北侧", "0.30 MPa"], ["南侧", "0.28 MPa"]] }, children: [] } } }, }; const form = { version: "answer-ui@1", block_id: "report-form", phase: "final", fallback_text: "可补充现场记录。", spec: { root: "form", elements: { form: { type: "FollowUpForm", props: { title: "补充现场资料", description: "由用户主动补充。", fields: [{ name: "record", label: "现场记录", kind: "text", required: true }] }, children: [] } } }, }; const headers = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, PUT, POST, OPTIONS", "Access-Control-Allow-Headers": "Authorization, Content-Type, X-Project-Id" }; await page.route(`**/api/v1/session/${sessionId}?**`, (route) => route.fulfill({ headers, json: { session_id: sessionId, title, purpose: "ordinary_chat", status: "active", is_streaming: false, messages: [{ id: "anchored", role: "assistant", parts: [ { type: "text", text: "正在核对查询记录。" }, ...[process, table, form].map((block) => ({ type: "data-answer_ui", id: block.block_id, data: block })), { type: "data-final_answer", data: { content: "前置结论\n\n[[ui:report-table]]\n\n表后分析\n\n[[ui:report-form]]\n\n行动建议", ui_block_ids: ["report-table", "query-process", "report-form"] } }, ] }], } })); await page.route(`**/api/v1/session/${sessionId}/interactions**`, (route) => route.fulfill({ headers, json: { items: [] } })); const records = new Map(); const reads: string[] = []; await page.route(`**/api/v1/session/${sessionId}/ui-state**`, async (route) => { if (route.request().method() === "OPTIONS") return route.fulfill({ status: 204, headers }); const input = route.request().method() === "PUT" ? route.request().postDataJSON() as AnswerViewStateWrite : null; const blockId = input?.block_id ?? new URL(route.request().url()).searchParams.get("block_id")!; reads.push(blockId); const current = records.get(blockId) ?? { version: "answer-view-state@1" as const, message_id: "anchored", block_id: blockId, revision: 0, schema_digest: "fixture", nodes: {}, block: { evidenceFilter: "all" }, updated_at: null }; const next = input ? { ...current, nodes: input.nodes, block: input.block, revision: current.revision + 1, updated_at: new Date().toISOString() } : current; records.set(blockId, next); await route.fulfill({ headers, json: next }); }); const openHistory = async () => { await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); await panel.getByRole("button", { name: /历史/ }).click(); await page.locator("button.agent-history-session-trigger").filter({ hasText: title }).click(); return panel.locator('[data-agent-message-id="anchored"]'); }; await page.goto("/", { waitUntil: "domcontentloaded" }); let message = await openHistory(); await expect(message.getByText("前置结论", { exact: true })).toBeVisible(); const finalTable = message.locator('[data-answer-ui="report-table"]'); const finalForm = message.locator('[data-answer-ui="report-form"]'); await expect(finalTable).toHaveAttribute("data-answer-phase", "final"); const ordered = await message.evaluate((element) => [...element.querySelectorAll('.agent-streaming-response, [data-answer-ui]')] .filter((node) => !node.closest("details")) .map((node) => node.getAttribute("data-answer-ui") ?? node.textContent?.trim())); expect(ordered).toEqual(["前置结论", "report-table", "表后分析", "report-form", "行动建议"]); await expect(message.locator('[data-answer-ui="query-process"]')).toBeHidden(); await finalTable.getByRole("textbox", { name: "搜索最终读数" }).fill("北侧"); await finalForm.getByLabel("现场记录(必填)").fill("未提交的现场草稿"); await expect.poll(() => records.get("report-form")?.nodes.form).toMatchObject({ type: "FollowUpForm", values: { record: "未提交的现场草稿" } }); await expect.poll(() => records.get("report-table")?.nodes.table).toMatchObject({ type: "Table", query: "北侧" }); await message.getByText("查看处理过程", { exact: true }).click(); await expect(message.locator('[data-answer-ui="query-process"]')).toBeVisible(); expect(reads).not.toContain("query-process"); expect(reads).not.toContain("anchored-process"); await message.getByText("查看处理过程", { exact: true }).click(); await context.grantPermissions(["clipboard-read", "clipboard-write"]); await message.getByRole("button", { name: "复制回复", exact: true }).click(); const copied = await page.evaluate(() => navigator.clipboard.readText()); expect(copied).toContain("最终读数"); expect(copied).not.toContain("中间查询资料"); expect(copied).not.toContain("[[ui:"); await page.getByRole("button", { name: "关闭通知", exact: true }).evaluateAll((buttons) => buttons.forEach((button) => (button as HTMLButtonElement).click())); await finalTable.screenshot({ path: `/tmp/agent-ui-anchors-table-${width}.png` }); await finalForm.screenshot({ path: `/tmp/agent-ui-anchors-form-${width}.png` }); await message.getByText("前置结论", { exact: true }).scrollIntoViewIfNeeded(); await page.getByRole("complementary", { name: "Agent 命令面板" }).screenshot({ path: `/tmp/agent-ui-anchors-${width}.png` }); await page.reload({ waitUntil: "domcontentloaded" }); message = await openHistory(); await expect(message.getByLabel("现场记录(必填)")).toHaveValue("未提交的现场草稿"); await expect(message.getByRole("textbox", { name: "搜索最终读数" })).toHaveValue("北侧"); expect(reads).not.toContain("query-process"); expect(await message.evaluate((element) => element.scrollWidth > element.clientWidth)).toBe(false); }); }