Files
next-tjwater-frontend/tests/browser/agent-intelligent-workspace.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

253 lines
16 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 { expect, test } from "@playwright/test";
import { mockAgentApi } from "./support/mock-agent-api";
import { mockWorkbenchContext } from "./support/mock-workbench-context";
import { responsesFixture } from "./support/responses-fixture";
// Explicitly synthetic evidence used only to exercise the generated component tree.
const comparison = {
version: "answer-ui@1",
block_id: "pressure-comparison",
fallback_text: "演练:北区压力0.2130MPa,南区0.2870MPa;待补充现场核验。",
spec: {
root: "root",
elements: {
root: { type: "Stack", props: { columns: "one", gap: "normal" }, children: ["caution", "metrics", "records", "evidence", "inference", "location", "form"] },
caution: { type: "Text", props: { text: "以下为浏览器演练资料,不能据此执行现场调度。", tone: "warning" }, children: [] },
metrics: { type: "Stack", props: { columns: "two", gap: "compact" }, children: ["north", "south"] },
north: { type: "Metric", props: { label: "北区压力", value: "0.2130", unit: "MPa", source: "演练监测快照" }, children: [] },
south: { type: "Metric", props: { label: "南区压力", value: "0.2870", unit: "MPa", source: "演练监测快照" }, children: [] },
records: { type: "Table", props: {
title: "演练监测记录", columns: ["监测对象", "压力(MPa)", "记录时间"],
rows: [["演练北区入口", "0.2130", "08:10"], ["演练南区入口", "0.2870", "08:10"]],
}, children: [] },
evidence: { type: "Evidence", props: {
title: "演练记录来源", summary: "这组数据来自浏览器测试,不代表现场核实。", source: "浏览器验收数据",
kind: "demo", observed_at: "2026-10-26T08:10:00+08:00", evidence_id: "DEMO-PRESSURE-26",
}, children: [] },
inference: { type: "Evidence", props: {
title: "待核实的压力判断", summary: "单次压力差异不足以确认漏损,需要后续核查。", source: "基于演练资料的推断",
kind: "inference", observed_at: null, evidence_id: null,
}, children: [] },
location: { type: "MapLocation", props: { label: "定位演练北区入口", layer: "junctions", feature_ids: ["DEMO-NORTH-26"], source: "演练对象" }, children: [] },
form: { type: "FollowUpForm", props: {
title: "补充现场核查", description: "输入的资料由你确认后发送。",
fields: [{ name: "site_record", label: "现场核查记录", kind: "text", required: true }],
}, children: [] },
},
},
};
for (const width of [1440, 320]) {
test(`keeps display-only answer data and form drafts while expanding at ${width}px`, async ({ page }, testInfo) => {
await page.setViewportSize({ width, height: 900 });
await page.emulateMedia({ reducedMotion: "reduce" });
await mockWorkbenchContext(page, undefined, undefined, {}, [], { runtimeMode: "backend" });
await mockAgentApi(page);
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 }> }>;
};
prompts.push(body.input.filter((item) => item.role === "user").at(-1)?.content
.filter((part) => part.type === "input_text").map((part) => part.text ?? "").join("") ?? "");
const answer = prompts.length === 1 ? "两处压力存在差异,仍需结合现场记录判断原因。" : "已收到补充问题,继续核对演练资料。";
await route.fulfill({
headers: { "Access-Control-Allow-Origin": "*", "Content-Type": "text/event-stream" },
body: responsesFixture([
{ type: "start", messageId: `intelligent-answer-${prompts.length}` },
{ type: "text-start", id: "answer" },
{ type: "text-delta", id: "answer", delta: answer },
{ type: "text-end", id: "answer" },
...(prompts.length === 1 ? [{ type: "data-answer_ui", id: comparison.block_id, data: comparison }] : []),
{ type: "data-final_answer", data: { content: answer, ui_block_ids: prompts.length === 1 ? [comparison.block_id] : [] } },
{ type: "finish", finishReason: "stop" },
]),
});
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click();
const panel = page.getByRole("complementary", { name: "Agent 命令面板", includeHidden: true });
const composer = panel.locator(".agent-panel-prompt-textarea");
await composer.fill("比较这组演练监测资料,协助我选择继续核查的依据。");
await panel.getByRole("button", { name: "发送 Agent 指令" }).click();
const answer = panel.locator('[data-answer-ui="pressure-comparison"]');
await expect(answer).toHaveCount(1);
await expect(answer.getByRole("note")).toContainText("浏览器演练资料");
await answer.getByRole("textbox", { name: "搜索演练监测记录" }).fill("北区");
await answer.getByRole("button", { name: /演练记录来源.*浏览器验收数据/ }).click();
await answer.getByLabel("现场核查记录(必填)").fill("留待核对的现场草稿");
await expect(answer.getByRole("button", { name: "收起补充资料", exact: true })).toHaveCount(0);
await expect(answer.getByText("直接探索已加载数据", { exact: true })).toHaveCount(0);
await expect(answer.getByText("区分记录、推断与用户资料", { exact: true })).toHaveCount(0);
await expect(answer.getByLabel("现场核查记录(必填)")).toHaveValue("留待核对的现场草稿");
await expect(answer.getByRole("textbox", { name: "搜索演练监测记录" })).toHaveValue("北区");
expect(prompts).toHaveLength(1);
await panel.getByRole("button", { name: "展开查看", exact: true }).click();
const workspace = page.getByRole("region", { name: "展开报告工作区", exact: true });
await expect(workspace.getByLabel("现场核查记录(必填)")).toHaveValue("留待核对的现场草稿");
const backdrop = page.locator(".report-reading-backdrop");
await expect(backdrop).toBeVisible();
const backdropBounds = (await backdrop.boundingBox())!;
expect(backdropBounds).toEqual({ x: 0, y: 0, width, height: 900 });
const reportDialog = page.getByRole("dialog", { name: "对话中的分析报告", exact: true });
await expect(reportDialog).toBeVisible();
await expect(reportDialog).toHaveCSS("animation-name", "none");
await expect(reportDialog).toHaveCSS("border-radius", "12px");
const readingBounds = (await workspace.boundingBox())!;
expect(readingBounds.x).toBeGreaterThan(0);
expect(readingBounds.y).toBeGreaterThan(0);
expect(readingBounds.x + readingBounds.width).toBeLessThan(width);
expect(readingBounds.y + readingBounds.height).toBeLessThan(900);
expect(await workspace.evaluate((element) => element.scrollWidth > element.clientWidth)).toBe(false);
await page.evaluate(() => document.fonts.ready);
await page.getByRole("button", { name: "关闭通知", exact: true }).evaluateAll((buttons) => {
for (const button of buttons) (button as HTMLButtonElement).click();
});
await page.screenshot({ path: testInfo.outputPath(`intelligent-workspace-${width}.png`) });
await page.keyboard.press("Escape");
await expect(workspace).toHaveCount(0);
await expect(backdrop).toHaveCount(0);
await expect(answer.getByRole("checkbox")).toHaveCount(0);
await expect(answer.getByRole("button", { name: /加入分析|针对.*提问/ })).toHaveCount(0);
await expect(answer.getByRole("region", { name: "分析内容篮" })).toHaveCount(0);
expect(prompts).toHaveLength(1);
const horizontalOverflow = await panel.evaluate((element) => element.scrollWidth > element.clientWidth);
expect(horizontalOverflow).toBe(false);
await composer.fill("核查".repeat(5001));
await panel.getByRole("button", { name: "发送 Agent 指令" }).click();
await expect(panel.getByRole("alert")).toContainText("超过 10,000 字");
await expect(composer).toHaveValue("核查".repeat(5001));
expect(prompts).toHaveLength(1);
await composer.fill("根据这份演练资料继续核查北区压力。");
await panel.getByRole("button", { name: "发送 Agent 指令" }).click();
await expect.poll(() => prompts.length).toBe(2);
expect(prompts[1]).toContain("根据这份演练资料继续核查北区压力");
expect(prompts[1]).not.toContain("引用内容块");
await expect(panel.getByText("已收到补充问题,继续核对演练资料。", { exact: true })).toBeVisible();
if (width === 1440) {
await page.emulateMedia({ reducedMotion: "no-preference" });
await panel.getByRole("button", { name: "展开查看", exact: true }).click();
await expect(reportDialog).toBeVisible();
await expect(reportDialog).not.toHaveCSS("animation-name", "none");
await reportDialog.evaluate(async (element) => {
await Promise.allSettled(element.getAnimations({ subtree: true }).filter((animation) => Number.isFinite(Number(animation.effect?.getComputedTiming().endTime))).map((animation) => animation.finished));
});
await expect(workspace.getByLabel("现场核查记录(必填)")).toHaveValue("留待核对的现场草稿");
await page.keyboard.press("Escape");
await expect(reportDialog).toHaveCount(0);
await expect(panel.getByRole("button", { name: "展开查看", exact: true })).toBeFocused();
}
});
}
const markdownReport = `## 演练压力核查报告
这是一份浏览器演练报告。请结合记录时间核查压力差异,并保留原始精度与单位。
### 监测记录
| 监测对象 | 压力(MPa) | 记录时间 | 核查状态 |
| --- | ---: | --- | --- |
| 演练北区入口 | 0.2130 | 08:10 | 待现场核查 |
| 演练南区入口 | 0.2870 | 08:10 | 待现场核查 |
> 这些数据仅用于界面验收,不代表真实供水情况。
### 核查流程
\`\`\`mermaid
flowchart LR
A[核对监测记录] --> B[比较压力差异]
B --> C[补充现场资料]
C --> D[人工复核结论]
\`\`\`
`;
for (const width of [1440, 320]) {
test(`markdown report tables and diagrams stay readable in bounded windows 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);
await page.route("**/api/v1/responses", async (route) => {
if (route.request().method() === "OPTIONS") return route.fallback();
await route.fulfill({
headers: { "Access-Control-Allow-Origin": "*", "Content-Type": "text/event-stream" },
body: responsesFixture([
{ type: "start", messageId: "markdown-reading" },
{ type: "text-start", id: "answer" },
{ type: "text-delta", id: "answer", delta: markdownReport },
{ type: "text-end", id: "answer" },
{ type: "data-final_answer", data: { content: markdownReport, ui_block_ids: [] } },
{ type: "finish", finishReason: "stop" },
]),
});
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click();
const panel = page.getByRole("complementary", { name: "Agent 命令面板", includeHidden: true });
await panel.locator(".agent-panel-prompt-textarea").fill("请整理演练压力报告。");
await panel.getByRole("button", { name: "发送 Agent 指令" }).click();
await expect(panel.getByRole("heading", { name: "演练压力核查报告" })).toBeVisible();
await expect(panel.locator(".agent-chat-message-header")).toHaveCount(0);
const paragraph = panel.getByText("这是一份浏览器演练报告。请结合记录时间核查压力差异,并保留原始精度与单位。", { exact: true });
expect(await paragraph.evaluate((element) => Number.parseFloat(getComputedStyle(element).fontSize))).toBeGreaterThanOrEqual(15);
const table = panel.locator('[data-streamdown="table-wrapper"]');
await expect(table.getByRole("cell", { name: "0.2130", exact: true })).toBeVisible();
const graph = panel.locator("[data-markdown-diagram]");
// Streamdown defers diagram rendering until the block enters the viewport.
await graph.scrollIntoViewIfNeeded();
await expect(graph.locator("svg[id]")).toHaveCount(1);
const assertBounded = async (dialog: ReturnType<typeof page.getByRole>) => {
await expect(dialog).toBeVisible();
await dialog.evaluate(async (element) => {
await Promise.allSettled(element.getAnimations().filter((animation) => Number.isFinite(Number(animation.effect?.getComputedTiming().endTime))).map((animation) => animation.finished));
});
const bounds = (await dialog.boundingBox())!;
expect(bounds.x).toBeGreaterThan(0);
expect(bounds.y).toBeGreaterThan(0);
expect(bounds.x + bounds.width).toBeLessThan(width);
expect(bounds.y + bounds.height).toBeLessThan(900);
expect(await dialog.evaluate((element) => element.scrollWidth > element.clientWidth)).toBe(false);
};
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
await table.getByRole("button", { name: "复制表格", exact: true }).click();
expect(await page.evaluate(() => navigator.clipboard.readText())).toContain("0.2130");
await table.getByRole("button", { name: /放大查看|View fullscreen/ }).click();
const tableDialog = page.getByRole("dialog");
await assertBounded(tableDialog);
await expect(tableDialog).not.toContainText("完整数据保留在表格内");
await expect(table.locator("table")).toBeVisible();
await expect(table.locator("table")).toContainText("0.2130");
await expect(table.getByRole("button", { name: "放大查看表格", exact: true, includeHidden: true })).toHaveText("");
await expect(tableDialog.getByRole("cell", { name: "0.2130", exact: true })).toBeVisible();
await page.evaluate(() => document.fonts.ready);
await page.getByRole("button", { name: "关闭通知", exact: true, includeHidden: true }).evaluateAll((buttons) => { for (const button of buttons) (button as HTMLButtonElement).click(); });
await page.screenshot({ path: testInfo.outputPath(`markdown-table-${width}.png`) });
await page.keyboard.press("Escape");
await expect(tableDialog).toHaveCount(0);
await expect(table.getByRole("button", { name: "放大查看表格", exact: true })).toBeFocused();
await graph.getByRole("button", { name: /放大查看|View fullscreen/ }).click();
const graphDialog = page.getByRole("dialog");
await assertBounded(graphDialog);
await expect(graphDialog.locator("svg[id]")).toHaveCount(1);
await graphDialog.getByRole("button", { name: "Zoom in", exact: true }).click();
const zoom = "matrix(1.1, 0, 0, 1.1, 0, 0)";
await expect(graphDialog.locator('[role="application"]')).toHaveCSS("transform", zoom);
const labelBounds = (await graphDialog.locator(".nodeLabel").first().boundingBox())!;
expect(labelBounds.height).toBeGreaterThanOrEqual(18);
await page.screenshot({ path: testInfo.outputPath(`markdown-mermaid-${width}.png`) });
await page.keyboard.press("Escape");
await expect(graphDialog).toHaveCount(0);
await expect(graph.getByRole("button", { name: "放大查看图示", exact: true })).toBeFocused();
await expect(graph.locator('[role="application"]')).toHaveCSS("transform", zoom);
expect(await panel.evaluate((element) => element.scrollWidth > element.clientWidth)).toBe(false);
});
}