Files
next-tjwater-frontend/tests/browser/agent-answer-ui.e2e.ts
T
jiang c0e4231b83
Generic Container CI/CD / test-build-publish (push) Successful in 1m52s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m52s
feat: expand intelligent investigation workspace
2026-10-30 17:30:19 +08:00

260 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { responsesFixture } from "./support/responses-fixture";
import { expect, test } from "@playwright/test";
import { mockAgentApi } from "./support/mock-agent-api";
import { mockWorkbenchContext } from "./support/mock-workbench-context";
import { answerUiDemoBlock as block } from "./support/answer-ui-demo";
import { captureReadingGeometry } from "./support/reading-geometry";
for (const width of [1440, 375, 320]) {
test(`local rich answers stream, restore and submit at ${width}px`, async ({ page, context }, testInfo) => {
await page.setViewportSize({ width, height: 900 });
await page.emulateMedia({ reducedMotion: "reduce" });
await mockWorkbenchContext(page, undefined, undefined, {}, [], { runtimeMode: "backend" });
await mockAgentApi(page, {
sessions: [
{
session_id: "browser-history-1",
title: "演练结构化回答",
created_at: Date.now(),
updated_at: Date.now()
}
]
});
// Restored sessions must complete the pending-interaction check before sending.
await page.route("**/api/v1/session/*/interactions", async (route) => {
if (route.request().method() !== "GET") return route.fallback();
await route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: { items: [] } });
});
const parts = [
{ type: "text", text: "以下资料均为演练数据。" },
{ type: "data-answer_ui", id: "pressure", data: block },
{ type: "text", text: "仍需用户补充现场核验资料。" },
{
type: "data-final_answer",
data: { content: "仍需用户补充现场核验资料。", ui_block_ids: ["pressure"] }
}
];
await page.route("**/api/v1/session/browser-history-1?**", async (route) => {
if (route.request().method() !== "GET") return route.fallback();
await route.fulfill({
headers: { "Access-Control-Allow-Origin": "*" },
json: {
session_id: "browser-history-1",
title: "演练结构化回答",
purpose: "ordinary_chat",
status: "active",
is_streaming: false,
messages: [
{
id: "user-fixture",
role: "user",
parts: [{ type: "text", text: "查看演练监测资料" }]
},
{ id: "rich-answer", role: "assistant", parts }
]
}
});
});
const prompts: string[] = [];
await page.route("**/api/v1/responses", async (route) => {
if (route.request().method() === "OPTIONS") return route.fallback();
const body = route.request().postDataJSON() as {
input: Array<{ role: string; content: Array<{ type: string; text?: string }> }>;
};
const text =
body.input
.filter((m) => m.role === "user")
.at(-1)
?.content.filter((p) => p.type === "input_text")
.map((p) => p.text)
.join("") ?? "";
prompts.push(text);
const chunks =
prompts.length === 1
? [
{ type: "start", messageId: "rich-answer" },
{ type: "text-start", id: "answer" },
{ type: "text-delta", id: "answer", delta: parts[0].text },
{ type: "text-end", id: "answer" },
{ ...parts[1], data: { ...block, fallback_text: "演练初始数据" } },
parts[1],
{ type: "text-start", id: "answer-1" },
{ type: "text-delta", id: "answer-1", delta: parts[2].text },
{ type: "text-end", id: "answer-1" },
parts[3],
{ type: "finish", finishReason: "stop" }
]
: [
{ type: "start", messageId: "form-answer" },
{ type: "text-start", id: "answer" },
{ type: "text-delta", id: "answer", delta: "已收到补充资料,尚未保存业务证据。" },
{ type: "text-end", id: "answer" },
{ type: "finish", finishReason: "stop" }
];
await route.fulfill({
headers: {
"Access-Control-Allow-Origin": "*",
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache"
},
body: responsesFixture(chunks)
});
});
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 指令" }).click();
const ui = panel.locator('[data-answer-ui="pressure"]');
await expect(ui).toHaveCount(1);
const process = panel.locator("details[data-agent-process]");
const processToggle = panel.getByRole("button", { name: "查看处理过程", exact: true });
await expect(process).not.toHaveAttribute("open");
await expect(panel.getByText("以下资料均为演练数据。", { exact: true })).toBeHidden();
await processToggle.click();
await expect(panel.getByText("以下资料均为演练数据。", { exact: true })).toBeVisible();
await processToggle.click();
if (width >= 1024) {
const handle = page.getByRole("separator", { name: "调整 Agent 面板宽度" });
for (const panelWidth of [400, 640]) {
const box = (await handle.boundingBox())!;
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(panelWidth, box.y + box.height / 2);
await page.mouse.up();
await expect.poll(async () => Math.round((await panel.boundingBox())!.width)).toBe(panelWidth);
const columns = await ui
.locator(".answer-ui-stack-comparison")
.evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(" ").length);
expect(columns).toBe(2);
}
}
await expect(ui.getByText("0.30", { exact: false }).first()).toBeVisible();
await expect(panel.getByText("交互回答", { exact: true })).toHaveCount(0);
const contentNav = ui.getByRole("navigation", { name: "回答内容导航" });
const browseAnswer = contentNav.getByRole("button", { name: "浏览回答" });
await browseAnswer.click();
await expect(page.getByRole("menuitem", { name: "查看指标,2项" })).toBeVisible();
await page.keyboard.press("Escape");
await expect(browseAnswer).toBeFocused();
await page.keyboard.press("Enter");
await page.getByRole("menuitem", { name: "查看补充资料,1项" }).focus();
await page.keyboard.press("Enter");
await expect(ui.locator(".answer-ui-form")).toBeFocused();
await ui.getByLabel("现场位置(必填)").fill("导航保留的现场草稿");
await browseAnswer.click();
await page.getByRole("menuitem", { name: "查看趋势,1项" }).click();
await expect(ui.locator(".answer-ui-chart")).toBeFocused();
await expect(ui.getByLabel("现场位置(必填)")).toHaveValue("导航保留的现场草稿");
await panel.locator(".agent-conversation-scroll").evaluate((element) => { element.scrollTop = 0; });
await expect(panel.getByText("查看演练监测资料", { exact: true })).toBeVisible();
await panel.screenshot({ path: `test-results/agent-chat-redesign-${width}.png` });
await ui.getByRole("group", { name: /按回车查看数据点/ }).focus();
await page.keyboard.press("Enter");
await ui.getByRole("combobox", { name: "选择数据点" }).click();
await page.getByRole("option", { name: "09:00", exact: true }).click();
await expect(ui.getByRole("region", { name: "数据点详情" }).locator("dl")).toContainText("0.28");
await ui.getByRole("button", { name: "关闭数据点详情", exact: true }).click();
await expect(ui.getByRole("combobox", { name: "选择数据点" })).toBeHidden();
const lineTab = ui.getByRole("tab", { name: "折线", exact: true });
await lineTab.focus();
await page.keyboard.press("ArrowRight");
await expect(ui.getByRole("tab", { name: "柱状", exact: true })).toBeFocused();
await page.keyboard.press("ArrowRight");
await expect(ui.getByRole("tab", { name: "数据", exact: true })).toHaveAttribute("aria-selected", "true");
await expect(ui.getByRole("table")).toContainText("0.28");
await ui.getByRole("button", { name: "参考压力(MPa)", exact: true }).click();
await expect(ui.getByRole("columnheader", { name: /参考压力/ })).toHaveCount(0);
await ui.getByRole("button", { name: "参考压力(MPa)", exact: true }).click();
await expect(ui.getByRole("columnheader", { name: /参考压力/ })).toBeVisible();
await expect(ui.getByRole("combobox", { name: "区间起点" })).toBeHidden();
await ui.getByRole("button", { name: "筛选数据范围" }).click();
await ui.getByRole("combobox", { name: "区间起点" }).click();
await page.getByRole("option", { name: "09:00", exact: true }).click();
await expect(ui.getByRole("table")).not.toContainText("08:00");
await ui.getByRole("button", { name: "筛选数据范围" }).click();
await expect(ui.getByText("09:00 至 09:00", { exact: true })).toBeVisible();
await expect(ui.getByRole("table")).not.toContainText("08:00");
await ui.getByRole("button", { name: "筛选数据范围" }).click();
await ui.getByRole("button", { name: "重置范围" }).click();
await expect(ui.getByRole("table")).toContainText("08:00");
await ui.getByRole("button", { name: "筛选数据范围" }).click();
expect(prompts).toHaveLength(1);
const evidence = ui.getByRole("button", { name: /查看演练证据/ });
await evidence.focus();
await page.keyboard.press("Enter");
await expect(ui.getByText("仅用于组件验收,不代表已核验现场事实。")).toBeVisible();
await expect(panel.getByText("仍需用户补充现场核验资料。", { exact: true })).toHaveCount(1);
const bounds = await ui.evaluate((element) => {
const rect = element.getBoundingClientRect();
const panel = element.closest("aside")!.getBoundingClientRect();
return rect.left >= panel.left && rect.right <= panel.right;
});
expect(bounds).toBe(true);
await ui.getByRole("table").scrollIntoViewIfNeeded();
await panel.screenshot({ path: `test-results/answer-ui-${width}.png` });
const copy = panel.getByRole("button", { name: "复制回复", exact: true }).first();
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
await copy.click();
await expect(panel.getByRole("button", { name: "回复已复制", exact: true }).first()).toBeVisible();
expect(await page.evaluate(() => navigator.clipboard.readText())).toContain("仍需用户补充现场核验资料。");
// The same editor and data filter move into the workspace and back.
await ui.getByLabel("现场位置(必填)").fill("尚未提交的现场位置");
await ui.getByRole("tab", { name: "数据", exact: true }).click();
await ui.getByRole("textbox", { name: "搜索演练监测趋势" }).fill("09:00");
await panel.getByRole("button", { name: "展开查看", exact: true }).click();
const workspace = page.getByRole("region", { name: "展开报告工作区", exact: true });
await expect(workspace).toBeVisible();
await expect(page.getByLabel("现场位置(必填)")).toHaveCount(1);
await expect(workspace.getByLabel("现场位置(必填)")).toHaveValue("尚未提交的现场位置");
await expect(workspace.getByRole("textbox", { name: "搜索演练监测趋势" })).toHaveValue("09:00");
await workspace.getByRole("tab", { name: "折线", exact: true }).click();
for (const close of await page.getByRole("button", { name: "关闭通知", exact: true }).all()) await close.click();
await page.evaluate(() => document.fonts.ready);
const geometry = await captureReadingGeometry(workspace, testInfo);
expect(geometry.metricsHeight).toBeLessThan(145);
// The expanded reader reserves room for larger text and the task heading.
// The chart still starts within one viewport, and the document scrolls inside its window.
expect(geometry.chartOffset).toBeLessThan(600);
const readerBounds = (await workspace.boundingBox())!;
expect(readerBounds.x).toBeGreaterThan(0);
expect(readerBounds.y).toBeGreaterThan(0);
expect(readerBounds.x + readerBounds.width).toBeLessThan(width);
expect(readerBounds.y + readerBounds.height).toBeLessThan(900);
expect(geometry.horizontalOverflow).toBe(false);
await page.screenshot({ path: `test-results/answer-workspace-${width}.png` });
await workspace.getByRole("tab", { name: "数据", exact: true }).click();
await page.keyboard.press("Escape");
await expect(workspace).toHaveCount(0);
await expect(ui.getByLabel("现场位置(必填)")).toHaveValue("尚未提交的现场位置");
await expect(panel.getByRole("button", { name: "展开查看", exact: true })).toBeFocused();
await panel.getByRole("button", { name: "展开查看", exact: true }).click();
await expect(workspace.getByRole("checkbox")).toHaveCount(0);
await expect(workspace.getByRole("button", { name: /加入分析|针对.*提问/ })).toHaveCount(0);
await page.keyboard.press("Escape");
await expect(workspace).toHaveCount(0);
expect(prompts).toHaveLength(1);
// Leave the current editor before restoring canonical persisted history.
await panel.getByRole("button", { name: "新建 Agent 对话" }).click();
await expect(ui).toHaveCount(0);
await panel.getByRole("button", { name: /历史/ }).click();
await page
.locator("button.agent-history-session-trigger")
.filter({ hasText: "演练结构化回答" })
.click();
await expect(ui).toHaveCount(1);
await expect(ui.getByLabel("现场位置(必填)")).toHaveValue("");
expect(prompts).toHaveLength(1);
await ui.getByLabel("现场位置(必填)").fill("演练管段东侧");
await ui.getByRole("button", { name: "发送补充资料" }).click();
await expect.poll(() => prompts.length).toBe(2);
expect(prompts[1]).toContain("现场位置:演练管段东侧");
await expect(
panel.getByText("已收到补充资料,尚未保存业务证据。", { exact: true })
).toBeVisible();
});
}