Files
next-tjwater-frontend/tests/browser/non-map-panels-layout.e2e.ts
jiang ec23d44b02 feat(agent): support Responses steering and project-aware tools
Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running
turns active while users steer or queue follow-ups and edit pending delivery.
Refresh the model catalog before generation and preserve approval mode selection.

Integrate scoped investigation messaging/presentation, geographic place selection,
current Agent composer styles and supporting workbench contracts.

Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and
production build. Preserve direct Server/Agent routing and project authentication.
2026-10-19 18:46:58 +08:00

379 lines
19 KiB
TypeScript

import { expectThemeColor, readColorAlpha, readThemeColor } from "./support/theme-colors";
import { expect, test, type Locator, type Page } from "@playwright/test";
import { mockAgentApi } from "./support/mock-agent-api";
import {
createAnalysisRuntimeSnapshot,
MOCK_SPATIAL_ANALYSIS
} from "./support/mock-analysis-runtime";
import { mockScadaApi } from "./support/mock-scada-api";
test.beforeEach(async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await prepareWorkbench(page);
});
test("assigned desktop panels avoid native form controls and horizontal overflow", async ({
page
}) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.evaluate(() => document.fonts.ready);
await expect(page.locator("[data-sonner-toast]")).toHaveCount(0, { timeout: 15_000 });
const mapToolbar = page.getByRole("navigation", { name: "地图工具" });
await page.getByRole("button", { name: "工况任务", exact: true }).click();
const conditionPanel = page.getByRole("region", { name: "工况任务", exact: true });
await expect(conditionPanel).toBeVisible();
const [toolbarBox, conditionBox] = await Promise.all([
mapToolbar.boundingBox(),
conditionPanel.boundingBox()
]);
expect(toolbarBox).not.toBeNull();
expect(conditionBox).not.toBeNull();
expect(Math.abs(toolbarBox!.y - conditionBox!.y)).toBeLessThanOrEqual(1);
await page.getByRole("button", { name: "工况任务", exact: true }).click();
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]');
await expectContained(agentPanel, 1440);
await page.getByRole("button", { name: "打开 Agent 历史记录" }).click();
await expectContained(page.getByRole("region", { name: "Agent 历史记录" }), 1440);
await page.setViewportSize({ width: 1280, height: 900 });
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
const layerPanel = page.getByRole("region", { name: "图层" });
await expectContained(layerPanel, 1280);
await expect(layerPanel.getByText("监测点", { exact: true })).toBeVisible();
await expect(layerPanel.getByRole("button", { name: /压力监测点/ })).toBeVisible();
await expect(layerPanel.getByRole("button", { name: /流量监测点/ })).toBeVisible();
await expect(layerPanel.getByRole("button", { name: /水质监测点/ })).toBeVisible();
const sourceGisGroup = layerPanel.locator("details").filter({ hasText: "GIS 原始数据" });
await expect(sourceGisGroup).not.toHaveAttribute("open", "");
await sourceGisGroup.locator("summary").click();
await expect(sourceGisGroup).toHaveAttribute("open", "");
await expect(sourceGisGroup.getByText("2020-04-23 · 全网除头桥快照")).toBeVisible();
await expect(sourceGisGroup.getByText("2020-08-11 · 专项管线台账")).toBeVisible();
await expect(sourceGisGroup.getByText("1 要素", { exact: false }).first()).toBeVisible();
await sourceGisGroup.getByRole("button", { name: /GIS 管线/ }).click();
await sourceGisGroup.getByRole("button", { name: /DN100\+ 台账/ }).click();
for (const facilityLabel of [
"GIS 阀门",
"排水阀",
"流量仪",
"消火栓",
"接水点",
"用户水表",
"非居民水表"
]) {
await sourceGisGroup.getByRole("button", { name: new RegExp(facilityLabel) }).click();
}
await expect(sourceGisGroup).toHaveAttribute("open", "");
await page.screenshot({
path: "test-results/non-map-panels/1280-layer-panel.png",
fullPage: true
});
await page.getByRole("button", { name: /图例:查看当前地图符号/ }).click();
const legendPanel = page.getByRole("region", { name: "图例" });
await expectContained(legendPanel, 1280);
await expect(legendPanel.getByRole("region", { name: "模型管线" })).toBeVisible();
await expect(legendPanel.getByRole("region", { name: "供水设施" })).toBeVisible();
await expect(legendPanel.getByRole("region", { name: "监测点" })).toBeVisible();
const aprilSourcePipeLegend = legendPanel.getByRole("region", {
name: "原始 GIS 管线 · 2020-04-23"
});
await expect(aprilSourcePipeLegend).toBeVisible();
await expect(aprilSourcePipeLegend.getByText("DN600 及以上", { exact: true })).toBeVisible();
await expect(aprilSourcePipeLegend.getByText("DN300-DN599", { exact: true })).toBeVisible();
await expect(aprilSourcePipeLegend.getByText("DN300 以下", { exact: true })).toBeVisible();
await expect(aprilSourcePipeLegend.getByText("未标注口径", { exact: true })).toBeVisible();
const augustSourcePipeLegend = legendPanel.getByRole("region", {
name: "原始 GIS 管线 · 2020-08-11"
});
await expect(augustSourcePipeLegend).toBeVisible();
await expect(augustSourcePipeLegend.getByText("DN100 及以上", { exact: true })).toBeVisible();
const sourceFacilityLegend = legendPanel.getByRole("region", { name: "原始 GIS 设施" });
await expect(sourceFacilityLegend).toBeVisible();
await expect(sourceFacilityLegend.locator('img[src^="/map/gis-"]')).toHaveCount(7);
await expect(legendPanel.getByRole("region", { name: "分析覆盖" })).toHaveCount(0);
await expect(legendPanel.locator(".status-dot")).toHaveCount(0);
await expect(legendPanel.locator('svg line[stroke-width="5"]')).not.toHaveCount(0);
await expect(legendPanel.locator('svg line[stroke-width="2"]')).not.toHaveCount(0);
await page.screenshot({
path: "test-results/non-map-panels/1280-legend-panel.png",
fullPage: true
});
await sourceFacilityLegend.scrollIntoViewIfNeeded();
await page.screenshot({
path: "test-results/non-map-panels/1280-legend-source-gis.png",
fullPage: true
});
await page.getByRole("button", { name: /测量:距离与范围测量/ }).click();
const measurePanel = page.getByRole("region", { name: "测量", exact: true });
await expectContained(measurePanel, 1280);
await expect(measurePanel).toHaveCSS("width", "324px");
const meterUnit = measurePanel.getByRole("button", { name: "m", exact: true });
const kilometerUnit = measurePanel.getByRole("button", { name: "km", exact: true });
await expect(kilometerUnit).toHaveAttribute("aria-pressed", "true");
await expectThemeColor(kilometerUnit, "background-color", "--action-blue");
await meterUnit.click();
await expect(meterUnit).toHaveAttribute("aria-pressed", "true");
await expectThemeColor(meterUnit, "background-color", "--action-blue");
await kilometerUnit.click();
const distanceMode = measurePanel.getByRole("button", { name: "距离", exact: true });
const areaMode = measurePanel.getByRole("button", { name: "面积", exact: true });
const segmentMode = measurePanel.getByRole("button", { name: "管段", exact: true });
await expect(distanceMode).toHaveAttribute("aria-pressed", "true");
await areaMode.focus();
await expect(areaMode).toBeFocused();
await page.keyboard.press("Enter");
await expect(areaMode).toHaveAttribute("aria-pressed", "true");
await expect(measurePanel.getByText("当前面积", { exact: true })).toBeVisible();
await segmentMode.click();
await expect(segmentMode).toHaveAttribute("aria-pressed", "true");
await expect(measurePanel.getByText("资产长度", { exact: true })).toBeVisible();
await distanceMode.click();
await expect(measurePanel.getByRole("button", { name: "复制结果" })).toBeDisabled();
await expect(measurePanel.getByRole("button", { name: "清除测量" })).toBeDisabled();
await expect(measurePanel.getByText("等待开始", { exact: true })).toBeVisible();
const startMeasureButton = measurePanel.getByRole("button", { name: /开始测量/ });
await expect.poll(async () => readColorAlpha(await startMeasureButton.evaluate((element) => getComputedStyle(element).backgroundColor))).toBeCloseTo(0.864, 2);
await expect(startMeasureButton).not.toHaveCSS("background-color", await readThemeColor(startMeasureButton, "--action-blue"));
await expect(startMeasureButton).toContainText("在地图中连续取点");
await startMeasureButton.click();
const stopMeasureButton = measurePanel.getByRole("button", { name: /停止测量/ });
await expect(stopMeasureButton).toBeVisible();
await expect(stopMeasureButton).toHaveAttribute("data-state", "active");
await expectThemeColor(stopMeasureButton, "border-color", "--palette-brand");
await expect(stopMeasureButton).toContainText("暂停并保留当前读数");
await expect(stopMeasureButton).toContainText("进行中");
await expect(measurePanel.getByText("测量中", { exact: true })).toBeVisible();
await page.screenshot({
path: "test-results/non-map-panels/1280-measure-panel-active.png",
fullPage: true
});
await stopMeasureButton.click();
await page.mouse.move(640, 56);
const resumedStartMeasureButton = measurePanel.getByRole("button", { name: /开始测量/ });
await expect(resumedStartMeasureButton).toBeEnabled();
await expect.poll(async () => readColorAlpha(await resumedStartMeasureButton.evaluate((element) => getComputedStyle(element).backgroundColor))).toBeCloseTo(0.864, 2);
await expect(resumedStartMeasureButton).not.toHaveCSS("background-color", await readThemeColor(resumedStartMeasureButton, "--action-blue"));
await page.screenshot({
path: "test-results/non-map-panels/1280-measure-panel.png",
fullPage: true
});
await page.getByRole("button", { name: /标注:管理地图标注/ }).click();
await expectContained(page.getByRole("region", { name: "标注" }), 1280);
await page.screenshot({
path: "test-results/non-map-panels/1280-tool-panels.png",
fullPage: true
});
await page.getByRole("button", { name: "切换地图 Dev Panel" }).click();
const devPanel = page.getByRole("complementary", { name: "地图开发面板" });
await expectContained(devPanel, 1280);
await expect(devPanel.locator("select")).toHaveCount(0);
await devPanel.locator("summary").filter({ hasText: "图层样式" }).click();
const layerGroup = devPanel.getByRole("combobox", { name: "业务图层组" });
await layerGroup.focus();
await layerGroup.press("Enter");
const junctionOption = page.getByRole("option", { name: "节点" });
await expect(junctionOption).toBeVisible();
await junctionOption.focus();
await page.keyboard.press("Enter");
await expect(layerGroup).toHaveText("节点");
await expect(
devPanel.locator('input:not([type="range"]):not([type="color"]):not([type="checkbox"])').first()
).toHaveCSS("font-family", /Noto Sans SC/);
await page.screenshot({ path: "test-results/non-map-panels/1280-dev-panel.png", fullPage: true });
});
for (const width of [375, 320]) {
test(`mobile Agent and tool panels fit ${width}px without horizontal overflow`, async ({
page
}) => {
await page.setViewportSize({ width, height: 844 });
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.evaluate(() => document.fonts.ready);
await expect(page.locator("[data-sonner-toast]")).toHaveCount(0, { timeout: 15_000 });
await page.getByRole("button", { name: "打开 Agent 面板" }).click();
const agentSheet = page.getByRole("region", { name: "Agent 工作台抽屉" });
await expectContained(agentSheet, width);
await expect(agentSheet.getByRole("heading", { name: "我已就绪,请描述任务" })).toHaveCount(0);
await expect(agentSheet.getByRole("log")).toBeVisible();
await expect(agentSheet.getByRole("textbox")).toBeVisible();
await expect(page.locator("[data-sonner-toast]")).toHaveCount(0, { timeout: 15_000 });
await page.screenshot({
path: `test-results/non-map-panels/${width}-agent.png`,
fullPage: true
});
await page.getByRole("button", { name: "关闭 Agent 面板" }).click();
await page.getByRole("button", { name: "更多:更多地图操作" }).click();
const toolPanel = page.getByRole("region", { name: "更多" });
await expectContained(toolPanel, width);
await page.screenshot({
path: `test-results/non-map-panels/${width}-more.png`,
fullPage: true
});
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
const mobileLayerPanel = page.getByRole("region", { name: "图层" });
await expectContained(mobileLayerPanel, width);
await expect(mobileLayerPanel.getByRole("button", { name: /管线,按管径分级/ })).toBeVisible();
await expect(mobileLayerPanel.getByText("GIS 原始数据", { exact: true })).toBeAttached();
await page.screenshot({
path: `test-results/non-map-panels/${width}-layers.png`,
fullPage: true
});
await page.getByRole("button", { name: /图例:查看当前地图符号/ }).click();
const mobileLegendPanel = page.getByRole("region", { name: "图例" });
await expectContained(mobileLegendPanel, width);
await expect(mobileLegendPanel.getByRole("region", { name: "模型管线" })).toBeVisible();
await expect(mobileLegendPanel.getByRole("region", { name: "供水设施" })).toBeAttached();
await expect(mobileLegendPanel.getByRole("region", { name: "监测点" })).toBeAttached();
await expect(mobileLegendPanel.getByRole("region", { name: /原始 GIS 管线/ })).toHaveCount(0);
await page.screenshot({
path: `test-results/non-map-panels/${width}-legend.png`,
fullPage: true
});
await page.getByRole("button", { name: /测量:距离与范围测量/ }).click();
const mobileMeasurePanel = page.getByRole("region", { name: "测量", exact: true });
await expectContained(mobileMeasurePanel, width);
await expect(mobileMeasurePanel.getByRole("group", { name: "测量类型" })).toBeVisible();
await expect(mobileMeasurePanel.getByRole("button", { name: "复制结果" })).toBeDisabled();
await expect(mobileMeasurePanel.getByRole("button", { name: "清除测量" })).toBeDisabled();
await mobileMeasurePanel.getByRole("button", { name: "面积", exact: true }).click();
await expect(mobileMeasurePanel.getByText("当前面积", { exact: true })).toBeVisible();
await mobileMeasurePanel.getByRole("button", { name: /开始测量/ }).click();
await expect(mobileMeasurePanel.getByText("测量中", { exact: true })).toBeVisible();
await expect(mobileMeasurePanel.getByRole("button", { name: /停止测量/ })).toContainText("进行中");
await mobileMeasurePanel.getByRole("button", { name: /停止测量/ }).click();
await page.screenshot({
path: `test-results/non-map-panels/${width}-measure.png`,
fullPage: true
});
if (width === 375) {
await page.getByRole("button", { name: "打开工况任务" }).click();
const conditionSheet = page.getByRole("region", { name: "工况任务抽屉" });
await expectContained(conditionSheet, width);
await page.screenshot({
path: "test-results/non-map-panels/375-condition-feed.png",
fullPage: true
});
await conditionSheet
.getByRole("button", { name: /SCADA 数据诊断/ })
.first()
.click();
await expectContained(conditionSheet, width);
await page.screenshot({
path: "test-results/non-map-panels/375-condition-detail.png",
fullPage: true
});
}
});
}
for (const width of [1280, 375]) {
test(`artifact preview and document remain contained at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width: Math.max(width, 1280), height: 900 });
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.evaluate(() => document.fonts.ready);
await expect(page.locator("[data-sonner-toast]")).toHaveCount(0, { timeout: 15_000 });
await page.getByRole("button", { name: "打开最新后端分析成果" }).click();
await page.getByRole("button", { name: "将成果收起为预览" }).click();
await page.setViewportSize({ width, height: 900 });
const preview = page.getByRole("complementary", { name: "后端分析成果预览" });
await expectContained(preview, width);
await page.screenshot({
path: `test-results/non-map-panels/${width}-artifact-preview.png`,
fullPage: true
});
await preview.getByRole("button", { name: "打开成果" }).click();
const workspace = page.locator("#artifact-workspace");
await expectContained(workspace, width);
const heading = workspace.getByRole("heading", { name: "北区爆管定位" });
await expect(heading).toBeVisible();
await expect(heading).toHaveCSS("font-family", /Noto Sans SC/);
if (width === 1280) {
const [railBox, headingBox] = await Promise.all([
page.getByRole("complementary", { name: "Agent 折叠栏" }).boundingBox(),
heading.boundingBox()
]);
expect(railBox).not.toBeNull();
expect(headingBox).not.toBeNull();
expect(headingBox!.x).toBeGreaterThanOrEqual(railBox!.x + railBox!.width);
}
await expect(page.locator("[data-sonner-toast]")).toHaveCount(0, { timeout: 15_000 });
await page.screenshot({
path: `test-results/non-map-panels/${width}-artifact-document.png`,
fullPage: true
});
});
}
async function expectContained(locator: Locator, viewportWidth: number) {
await expect(locator).toBeVisible();
const box = await locator.boundingBox();
expect(box).not.toBeNull();
expect(box!.x).toBeGreaterThanOrEqual(0);
expect(box!.x + box!.width).toBeLessThanOrEqual(viewportWidth + 1);
expect(await locator.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(
true
);
}
async function prepareWorkbench(page: Page) {
// Layout fixtures keep the event stream open, just like the live backend.
await page.addInitScript(() => {
const fetch = window.fetch.bind(window);
window.fetch = (input, init) => {
const url = input instanceof Request ? input.url : String(input);
if (url.includes("/api/v1/workbench/events")) {
const body = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(": fixture keepalive\n\n"));
init?.signal?.addEventListener("abort", () => controller.close(), { once: true });
}
});
return Promise.resolve(new Response(body, { headers: { "Content-Type": "text/event-stream" } }));
}
return fetch(input, init);
};
});
await mockAgentApi(page);
await page.route("**/runtime-config.js", (route) =>
route.fulfill({
contentType: "application/javascript",
body: `globalThis.__TJWATER_CONFIG__=${JSON.stringify({
TJWATER_AUTH_MODE: "disabled",
TJWATER_MAP_URL: "https://non-map-panels.invalid/geoserver",
TJWATER_GEOSERVER_WORKSPACE: "tjwater_next",
TJWATER_SERVER_API_BASE_URL: "https://tjwater-api.invalid",
TJWATER_AGENT_URL: "http://127.0.0.1:8787",
TJWATER_ENABLE_DEV_PANEL: "true",
TJWATER_ENABLE_MSW: "false"
})}`
})
);
await page.route("https://non-map-panels.invalid/**", (route) => {
const url = new URL(route.request().url());
if (url.searchParams.get("resultType") === "hits") {
return route.fulfill({
contentType: "application/xml",
body: '<wfs:FeatureCollection numberMatched="1" numberReturned="0" />'
});
}
return route.fulfill({ status: 204 });
});
await page.route("https://demotiles.maplibre.org/**", (route) => route.fulfill({ status: 204 }));
await page.route("**/*.riv", (route) => route.abort());
await mockScadaApi(page, [], createAnalysisRuntimeSnapshot(MOCK_SPATIAL_ANALYSIS));
}