feat: polish intelligent answer interactions

This commit is contained in:
2026-10-29 18:39:50 +08:00
parent 9474788db6
commit 1ee21d1c7f
26 changed files with 1762 additions and 327 deletions
+88
View File
@@ -0,0 +1,88 @@
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";
// Synthetic readings exercise the same chart contract as the production answer.
const chart = {
version: "answer-ui@1", block_id: "chart-motion", fallback_text: "演练压力对比",
spec: { root: "chart", elements: { chart: { type: "Chart", children: [], props: {
title: "节点17038压力:事件日前后同时段对比(m)", chart_type: "line",
x_data: ["07:00", "07:15", "07:30", "07:45", "08:00", "08:15", "08:30", "08:45", "09:00"],
series: [{ name: "6月1日(m)", data: [57.8, 63, 64, 66, 67, 68, 71, 73.8, 73.93] },
{ name: "6月2日(m)", data: [58.91, 62, 63, 65, 65, 66, 69, 71, 71.34] }],
source: "浏览器演练数据,待现场核验",
} } } },
};
for (const width of [1440, 375]) for (const reduce of [false, true]) {
test(`chart switches with ${reduce ? "reduced" : "normal"} motion at ${width}px`, async ({ page }, testInfo) => {
await page.setViewportSize({ width, height: 1000 });
await page.emulateMedia({ reducedMotion: reduce ? "reduce" : "no-preference" });
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: "chart-answer" },
{ type: "data-answer_ui", id: chart.block_id, data: chart },
{ type: "data-final_answer", data: { content: "演练资料,待现场核验。", ui_block_ids: [chart.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 命令面板" });
await panel.locator("textarea").fill("查看演练压力对比");
await panel.getByRole("button", { name: "发送 Agent 指令" }).click();
const ui = panel.locator(".answer-ui-chart");
await expect(ui).toBeVisible();
await expect(ui.getByRole("button", { name: "查看数据点", exact: true })).toHaveCount(0);
await expect(ui.getByRole("region", { name: "数据点详情" })).toHaveCount(0);
const content = ui.locator("[data-chart-view]");
const samples = await ui.getByRole("tab", { name: "柱状", exact: true }).evaluate(async (tab) => {
const content = tab.closest(".answer-ui-chart")!.querySelector("[data-chart-view]")!;
const values: number[] = [];
// Dispatch a real pointer modality before click, sampling the actual render over frames.
tab.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true }));
tab.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, button: 0 }));
(tab as HTMLElement).click();
for (let i = 0; i < 16; i++) {
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
values.push(Number(getComputedStyle(content).opacity));
}
return values;
});
await expect(content).toHaveAttribute("data-chart-view", "bar");
if (reduce) expect(samples.every((value) => value === 1)).toBe(true);
else expect(samples.some((value) => value > 0 && value < 1)).toBe(true);
await expect(content).toHaveCSS("opacity", "1");
await ui.getByRole("tab", { name: "数据", exact: true }).click();
await expect(ui.getByRole("group", { name: "当前区间摘要" })).toHaveCount(0);
await ui.getByRole("textbox", { name: /搜索节点17038/ }).fill("08:30");
await ui.getByRole("checkbox", { name: "选择第 7 行", exact: true }).check();
const line = ui.getByRole("tab", { name: "折线", exact: true });
await line.focus();
await page.keyboard.press("Enter");
await expect(content).toHaveCSS("opacity", "1");
// Exercise the real SVG hit target as well as the keyboard alternative.
await ui.locator('svg path[stroke="#2563eb"]').last().click();
await expect(ui.getByRole("region", { name: "数据点详情" })).toContainText("73.93");
await ui.getByRole("button", { name: "关闭数据点详情" }).click();
const plot = ui.getByRole("group", { name: /按回车查看数据点/ });
await plot.focus();
await page.keyboard.press("End");
await expect(ui.getByRole("region", { name: "数据点详情" })).toContainText("73.93");
await ui.getByRole("button", { name: "关闭数据点详情" }).click();
await expect(plot).toBeFocused();
await ui.getByRole("tab", { name: "数据", exact: true }).click();
await expect(ui.getByRole("textbox", { name: /搜索节点17038/ })).toHaveValue("08:30");
await expect(ui.getByRole("checkbox", { name: "选择第 7 行", exact: true })).toBeChecked();
await line.click();
await expect(content).toHaveCSS("opacity", "1");
expect(await ui.evaluate((el) => el.scrollWidth > el.clientWidth)).toBe(false);
await page.evaluate(() => document.fonts.ready);
await ui.screenshot({ path: testInfo.outputPath(`chart-${width}-${reduce}.png`) });
});
}