refactor: simplify workbench layout and browser tests

This commit is contained in:
2026-08-18 17:06:42 +08:00
parent bdf4436899
commit bdd5eff776
12 changed files with 448 additions and 201 deletions
+19 -12
View File
@@ -1,4 +1,19 @@
import { expect, test, type Locator } from "@playwright/test";
import { mockAgentApi } from "./support/mock-agent-api";
test.beforeEach(async ({ page }) => {
await mockAgentApi(page, {
sessions: [
{
session_id: "browser-history-session",
title: "城南供水压力复核",
created_at: "2026-07-21T09:30:00+08:00",
updated_at: "2026-07-21T10:35:00+08:00",
run_status: "completed"
}
]
});
});
type BoundingBox = {
x: number;
@@ -12,9 +27,7 @@ async function waitForAnimations(locator: Locator) {
const animations = element
.getAnimations({ subtree: true })
.filter((animation) => animation.effect?.getTiming().iterations !== Infinity);
await Promise.all(
animations.map((animation) => animation.finished.catch(() => undefined))
);
await Promise.all(animations.map((animation) => animation.finished.catch(() => undefined)));
});
}
@@ -103,9 +116,7 @@ async function expectIntegratedHeader(header: Locator) {
expect(composition.borderRadius).toBe("0px");
}
test("desktop Agent header expands into one attached acrylic history surface", async ({
page
}) => {
test("desktop Agent header expands into one attached acrylic history surface", async ({ page }) => {
await page.goto("/", { waitUntil: "domcontentloaded" });
const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]');
@@ -194,16 +205,12 @@ test("desktop Agent header expands into one attached acrylic history surface", a
await expect(history).toBeHidden();
});
test("mobile Agent mirrors the desktop floating history surface", async ({
page
}) => {
test("mobile Agent mirrors the desktop floating history surface", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: "打开 Agent 面板" }).click();
const agentPanel = page
.locator('aside[aria-label="Agent 命令面板"]')
.filter({ visible: true });
const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]').filter({ visible: true });
await expect(agentPanel).toBeVisible();
await waitForAnimations(agentPanel);
const mobileSheet = page.getByRole("region", { name: "Agent 工作台抽屉" });
+5 -5
View File
@@ -1,4 +1,5 @@
import { expect, test } from "@playwright/test";
import { mockAgentApi } from "./support/mock-agent-api";
test("Agent panel resizes within its responsive 720px workspace limit", async ({ page }) => {
await page.goto("/", { waitUntil: "domcontentloaded" });
@@ -71,19 +72,18 @@ test.describe("mobile touch layout", () => {
});
});
// Temporarily disabled: this regression flow would start a real Agent run.
// Agent analysis must be explicitly triggered by a user, not by automated tests.
test.skip("mobile alert summary opens the Agent conversation panel", async ({ page }) => {
test("mobile alert summary opens the Agent conversation panel", async ({ page }) => {
await mockAgentApi(page);
await page.clock.setFixedTime(new Date("2026-07-21T10:40:00+08:00"));
await page.setViewportSize({ width: 375, height: 812 });
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: /查看异常处置面板/ }).click();
// Agent execution must remain an explicit user action.
// await page.getByRole("button", { name: "工况汇总" }).click();
await page.getByRole("button", { name: "工况汇总" }).click();
await expect(
page.locator('aside[aria-label="Agent 命令面板"]').filter({ visible: true })
).toBeVisible();
await expect(page.getByRole("button", { name: "关闭 Agent 面板" })).toBeVisible();
await expect(page.getByText("已发起工况汇总", { exact: true })).toBeVisible();
});
+17 -15
View File
@@ -7,7 +7,10 @@ test("submits a custom answer with the actionable id after a later tool placehol
await mockQuestionApi(page, replies);
await page.goto("/", { waitUntil: "domcontentloaded" });
const prompt = page.getByPlaceholder(/输入调度问题/).filter({ visible: true }).first();
const prompt = page
.getByPlaceholder(/输入调度问题/)
.filter({ visible: true })
.first();
await prompt.fill("测试 question 自定义回答");
await page
.getByRole("button", { name: "发送 Agent 指令" })
@@ -20,15 +23,17 @@ test("submits a custom answer with the actionable id after a later tool placehol
await panel.getByPlaceholder("输入自定义回答").fill("分析高峰时段压力");
await panel.getByRole("button", { name: "提交回答" }).click();
await expect.poll(() => replies).toEqual([
{
pathname: "/api/v1/agent/chat/question/question-1/reply",
body: {
session_id: "question-session",
answers: [["分析高峰时段压力"]]
await expect
.poll(() => replies)
.toEqual([
{
pathname: "/api/v1/agent/chat/question/question-1/reply",
body: {
session_id: "question-session",
answers: [["分析高峰时段压力"]]
}
}
}
]);
]);
await expect(panel.getByText("已回答", { exact: true })).toBeVisible();
await expect(panel.getByRole("button", { name: "提交回答" })).toHaveCount(0);
});
@@ -54,10 +59,7 @@ test("keeps the custom answer control within a 375px Agent sheet", async ({ page
expect(box!.x + box!.width).toBeLessThanOrEqual(375);
});
async function mockQuestionApi(
page: Page,
replies: Array<{ pathname: string; body: unknown }>
) {
async function mockQuestionApi(page: Page, replies: Array<{ pathname: string; body: unknown }>) {
await page.route("**/api/v1/agent/chat/**", async (route) => {
const request = route.request();
const pathname = new URL(request.url()).pathname;
@@ -119,8 +121,8 @@ function createQuestionStream() {
question: "你想分析哪一部分?",
options: [
{
label: "雨污混接分析",
description: "检查混接问题"
label: "供水分区压力分析",
description: "检查分区压力问题"
}
]
}
+13 -13
View File
@@ -36,7 +36,7 @@ test.beforeAll(async () => {
phase: "planning",
status: "running",
title: "正在分析运行上下文",
detail: "核对泵站、液位与降雨过程。",
detail: "核对泵站、液位与用水过程。",
started_at: Date.now()
}
});
@@ -47,8 +47,8 @@ test.beforeAll(async () => {
id: "inspect-rainfall",
phase: "evidence",
status: "completed",
title: "正在核对降雨过程与汇水区响应关系",
detail: "读取最近十二小时分钟级降雨序列,并检查每个汇水区的响应延迟是否处于合理范围。",
title: "正在核对用水过程与供水分区响应关系",
detail: "读取最近十二小时分钟级流量序列,并检查每个供水分区的响应延迟是否处于合理范围。",
started_at: Date.now()
}
});
@@ -74,7 +74,7 @@ test.beforeAll(async () => {
todos: [
{ id: "inspect-pump", content: "检查泵站运行状态", status: "in_progress" },
{ id: "inspect-level", content: "核对液位计连续性", status: "pending" },
{ id: "assess-overflow", content: "判断溢流风险", status: "pending" }
{ id: "assess-supply-risk", content: "判断供水风险", status: "pending" }
]
}
});
@@ -110,7 +110,7 @@ test.beforeAll(async () => {
id: "assess-risk",
phase: "analysis",
status: "completed",
title: "溢流风险判断完成",
title: "供水风险判断完成",
detail: "已完成风险点位排序。",
ended_at: Date.now()
}
@@ -131,7 +131,7 @@ test.beforeAll(async () => {
id: "assess-risk",
phase: "analysis",
status: "running",
title: "正在判断溢流风险",
title: "正在判断供水风险",
detail: "结合流量突变与液位趋势形成判断。",
started_at: Date.now()
}
@@ -382,7 +382,7 @@ test("stream completion and the plan card remain positionally stable", async ({
requestAnimationFrame(sample);
});
triggerProgressAppend();
await expect(liveProgress.getByText("正在判断溢流风险", { exact: true })).toBeVisible();
await expect(liveProgress.getByText("正在判断供水风险", { exact: true })).toBeVisible();
await expect(liveProgress.locator("li")).toHaveCount(3);
await expect(liveProgress.getByText("正在分析运行上下文", { exact: true })).toHaveCount(0);
await expect
@@ -450,7 +450,7 @@ test("stream completion and the plan card remain positionally stable", async ({
);
await expect(expandedLiveProgress.getByText("正在分析运行上下文", { exact: true })).toBeVisible();
await expect(
expandedLiveProgress.getByText("正在核对降雨过程与汇水区响应关系", { exact: true })
expandedLiveProgress.getByText("正在核对用水过程与供水分区响应关系", { exact: true })
).toHaveCSS("white-space", "normal");
await progressToggle.click();
await expect(page.getByRole("list", { name: "最近执行进度" }).locator("li")).toHaveCount(3);
@@ -468,7 +468,7 @@ test("stream completion and the plan card remain positionally stable", async ({
await expect(progressToggle.locator("../..")).toContainText("步骤4/4");
await expect(page.getByRole("list", { name: "最近执行进度" })).toHaveCount(0);
await expect(
page.getByText("溢流风险判断完成", { exact: true }).filter({ visible: true })
page.getByText("供水风险判断完成", { exact: true }).filter({ visible: true })
).toHaveCount(0);
await expect
.poll(async () =>
@@ -541,7 +541,7 @@ test("stream completion and the plan card remain positionally stable", async ({
await expect(
page.getByText("正在分析运行上下文", { exact: true }).filter({ visible: true }).first()
).toBeVisible();
const expandedLongTitle = expandedProgress.getByText("正在核对降雨过程与汇水区响应关系", {
const expandedLongTitle = expandedProgress.getByText("正在核对用水过程与供水分区响应关系", {
exact: true
});
await expect(expandedLongTitle).toHaveCSS("white-space", "normal");
@@ -562,7 +562,7 @@ test("stream completion and the plan card remain positionally stable", async ({
await expect(latestProgressRegion).toContainText("最近3/3");
triggerProgressAppend();
await expect(
page.getByRole("list", { name: "最近执行进度" }).getByText("正在判断溢流风险", { exact: true })
page.getByRole("list", { name: "最近执行进度" }).getByText("正在判断供水风险", { exact: true })
).toBeVisible();
await latestProgressToggle.click();
await expect(latestProgressRegion).toContainText("全部4/4");
@@ -570,7 +570,7 @@ test("stream completion and the plan card remain positionally stable", async ({
await expect(latestProgressRegion).toContainText("全部4/4", { timeout: 10_000 });
await expect(latestExpandedProgress).toBeVisible();
await expect(latestExpandedProgress.getByText("溢流风险判断完成", { exact: true })).toBeVisible();
await expect(latestExpandedProgress.getByText("供水风险判断完成", { exact: true })).toBeVisible();
const conversationScroll = page.locator(".agent-conversation-scroll");
await conversationScroll.hover();
@@ -673,7 +673,7 @@ function createLongMarkdownResponse() {
"当前管网运行状态总体稳定,但部分区域存在水位持续升高、流量突变和监测数据缺失。需要结合实时工况核对监测点连续性。\n\n";
content += "- 检查泵站运行状态与实时流量。\n";
content += "- 核对液位计连续性和异常峰值。\n";
content += "- 结合降雨过程判断溢流风险。\n\n";
content += "- 结合用水过程判断供水风险。\n\n";
}
return `${content}建议优先处置高风险点位,并持续跟踪后续变化。`;
}
+78
View File
@@ -0,0 +1,78 @@
import type { Page } from "@playwright/test";
type MockAgentApiOptions = {
sessions?: Array<Record<string, unknown>>;
};
export async function mockAgentApi(page: Page, options: MockAgentApiOptions = {}) {
await page.route("**/api/v1/agent/chat/**", async (route) => {
const pathname = new URL(route.request().url()).pathname;
if (pathname.endsWith("/stream")) {
await route.fulfill({
status: 200,
headers: {
"Cache-Control": "no-cache",
"Content-Type": "text/event-stream",
"X-Vercel-AI-UI-Message-Stream": "v1"
},
body: [
{ type: "start", messageId: "assistant-browser-test" },
{ type: "text-start", id: "answer" },
{ type: "text-delta", id: "answer", delta: "已收到工况汇总请求。" },
{ type: "text-end", id: "answer" },
{ type: "finish", finishReason: "stop" }
]
.map((part) => `data: ${JSON.stringify(part)}\n\n`)
.join("")
});
return;
}
if (pathname.endsWith("/sessions")) {
await route.fulfill({ json: { sessions: options.sessions ?? [] } });
return;
}
if (pathname.endsWith("/models")) {
await route.fulfill({
json: {
default_model: "test/model",
models: [
{
id: "test/model",
label: "测试模型",
description: "Playwright 浏览器测试",
icon: "bolt"
}
]
}
});
return;
}
if (pathname.endsWith("/ui-registry")) {
await route.fulfill({
json: {
schema_version: "agent-ui-registry@1",
chart_grammars: [],
components: [],
actions: []
}
});
return;
}
if (pathname.endsWith("/frontend-action-registry")) {
await route.fulfill({
json: {
schema_version: "frontend-action-registry@1",
actions: []
}
});
return;
}
await route.fulfill({ json: {} });
});
}
@@ -2,33 +2,30 @@ import { expect, test } from "@playwright/test";
test("opening Agent and condition panels does not move the map camera", async ({ page }) => {
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() =>
Boolean(window.__waterNetworkMap?.getLayer("scada-hit"))
await page.waitForFunction(
() =>
Boolean(window.__waterNetworkMap?.getLayer("scada-hit")) &&
!window.__waterNetworkMap?.isMoving()
);
await page.waitForTimeout(500);
await page.evaluate(() => {
const map = window.__waterNetworkMap;
document.documentElement.dataset.panelCameraMoveCount = "0";
map?.on("movestart", () => {
const currentCount = Number(
document.documentElement.dataset.panelCameraMoveCount ?? "0"
);
const currentCount = Number(document.documentElement.dataset.panelCameraMoveCount ?? "0");
document.documentElement.dataset.panelCameraMoveCount = String(currentCount + 1);
});
});
await page.getByRole("button", { name: "折叠 Agent 面板" }).click();
await page.waitForTimeout(250);
await expect(page.locator('aside[aria-label="Agent 折叠栏"]')).toBeVisible();
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
await page.waitForTimeout(250);
await expect(page.locator('aside[aria-label="Agent 命令面板"]')).toBeVisible();
const conditionPanel = page.getByRole("region", { name: "工况任务", exact: true });
await conditionPanel.getByRole("button", { name: "展开工况任务" }).click();
await page.waitForTimeout(250);
await expect(conditionPanel).toHaveCSS("width", "880px");
expect(
await page.evaluate(
() => document.documentElement.dataset.panelCameraMoveCount
)
).toBe("0");
expect(await page.evaluate(() => document.documentElement.dataset.panelCameraMoveCount)).toBe(
"0"
);
});
+6 -5
View File
@@ -176,14 +176,17 @@ async function openWorkbench(page: Page) {
await page.evaluate(async () => {
await document.fonts.ready;
});
await page.waitForTimeout(350);
await page.waitForFunction(() => Boolean(window.__waterNetworkMap?.getLayer("scada-hit")));
}
async function selectBasemap(page: Page, label: "浅色" | "影像") {
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
await page.getByRole("button", { name: label, exact: true }).click();
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
await page.waitForTimeout(220);
await expect(page.locator("main")).toHaveAttribute(
"data-basemap-tone",
label === "影像" ? "satellite" : "light"
);
}
async function expectDesktopFloatingGeometry(page: Page) {
@@ -243,9 +246,7 @@ async function expectAcrylicAlphas(
const alphas = await page.evaluate(() => {
return {
navigation: alphaOf(document.querySelector(".acrylic-navigation")),
agentPanel: alphaOf(
document.querySelector('aside[aria-label="Agent 命令面板"]')
),
agentPanel: alphaOf(document.querySelector('aside[aria-label="Agent 命令面板"]')),
conditionPanel: alphaOf(document.querySelector(".scheduled-feed-panel-shell")),
control: alphaOf(document.querySelector(".acrylic-control"))
};