230 lines
12 KiB
TypeScript
230 lines
12 KiB
TypeScript
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()
|
||
}
|
||
]
|
||
});
|
||
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")
|
||
.filter({ has: page.locator("summary", { hasText: "查看处理过程" }) });
|
||
await expect(process).not.toHaveAttribute("open");
|
||
await expect(panel.getByText("以下资料均为演练数据。", { exact: true })).toBeHidden();
|
||
await process.locator("summary").click();
|
||
await expect(panel.getByText("以下资料均为演练数据。", { exact: true })).toBeVisible();
|
||
await process.locator("summary").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 })).toBeVisible();
|
||
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` });
|
||
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 selection 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 ui.getByRole("checkbox", { name: "选择第 2 行" }).check();
|
||
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 expect(workspace.getByRole("checkbox", { name: "选择第 2 行" })).toBeChecked();
|
||
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);
|
||
expect(geometry.chartOffset).toBeLessThan(430);
|
||
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 workspace.locator("[data-interactive-table]").getByRole("button", { name: "针对这部分提问" }).click();
|
||
await expect(workspace).toHaveCount(0);
|
||
await panel.getByText("查看引用内容", { exact: true }).click();
|
||
await expect(panel.getByText("引用内容块:pressure", { exact: false })).toBeVisible();
|
||
expect(prompts).toHaveLength(1);
|
||
await panel.getByRole("button", { name: "移除内容引用" }).click();
|
||
// 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();
|
||
});
|
||
}
|