253 lines
16 KiB
TypeScript
253 lines
16 KiB
TypeScript
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);
|
||
});
|
||
}
|