94 lines
7.4 KiB
TypeScript
94 lines
7.4 KiB
TypeScript
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<string, AnswerViewStateRecord>();
|
|
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);
|
|
});
|
|
}
|