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.
341 lines
14 KiB
TypeScript
341 lines
14 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
|
import { mockAgentApi } from "./support/mock-agent-api";
|
|
import { mockScadaApi } from "./support/mock-scada-api";
|
|
import { readRenderedContrast, readThemeColor } from "./support/theme-colors";
|
|
|
|
const EMPTY_RASTER_TILE = Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
|
|
"base64"
|
|
);
|
|
|
|
test.describe("Main Frame workbench", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.clock.setFixedTime(new Date("2026-07-21T10:40:00+08:00"));
|
|
await page.emulateMedia({ reducedMotion: "reduce" });
|
|
await prepareDeterministicWorkbench(page);
|
|
});
|
|
|
|
test("desktop light basemap keeps the Main Frame workspace legible", async ({ page }) => {
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
await openWorkbench(page);
|
|
await openDesktopAgent(page);
|
|
|
|
await expectDesktopMainFrameGeometry(page);
|
|
await expectSolidMainFrameMaterials(page);
|
|
await expectLoadedChineseFont(page);
|
|
await expectBodyContrast(page);
|
|
await page.screenshot({ path: "test-results/palette/workbench-desktop-light.png", animations: "disabled" });
|
|
await expect(page).toHaveScreenshot("workbench-desktop-light.png", {
|
|
animations: "disabled",
|
|
maxDiffPixelRatio: 0.01
|
|
});
|
|
});
|
|
|
|
test("desktop satellite basemap preserves the solid Main Frame hierarchy", async ({ page }) => {
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
await openWorkbench(page);
|
|
await openDesktopAgent(page);
|
|
await selectBasemap(page, "影像");
|
|
|
|
await expect(page.locator("main")).toHaveAttribute("data-basemap-tone", "satellite");
|
|
await expectDesktopMainFrameGeometry(page);
|
|
await expectSolidMainFrameMaterials(page);
|
|
await page.screenshot({ path: "test-results/palette/workbench-desktop-satellite.png", animations: "disabled" });
|
|
await expect(page).toHaveScreenshot("workbench-desktop-satellite.png", {
|
|
animations: "disabled",
|
|
maxDiffPixelRatio: 0.01
|
|
});
|
|
});
|
|
|
|
test("desktop selector switches Tianditu vector and imagery layer pairs", async ({ page }) => {
|
|
await page.setViewportSize({ width: 1280, height: 800 });
|
|
await openWorkbench(page);
|
|
|
|
await selectBasemap(page, "天地图矢量");
|
|
await expectBaseLayerVisibility(page, {
|
|
"tianditu-vector": "visible",
|
|
"tianditu-vector-annotation": "visible",
|
|
"tianditu-image": "none",
|
|
"tianditu-image-annotation": "none"
|
|
});
|
|
|
|
await selectBasemap(page, "天地图影像");
|
|
await expectBaseLayerVisibility(page, {
|
|
"tianditu-vector": "none",
|
|
"tianditu-vector-annotation": "none",
|
|
"tianditu-image": "visible",
|
|
"tianditu-image-annotation": "visible"
|
|
});
|
|
});
|
|
|
|
test("map focus keeps pointer interaction borderless and uses the app focus style", async ({ page }) => {
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
await openWorkbench(page);
|
|
await openDesktopAgent(page);
|
|
const canvas = page.locator("#workspace-map .maplibregl-canvas");
|
|
const box = (await canvas.boundingBox())!;
|
|
await canvas.click({ position: { x: box.width / 2, y: box.height / 2 } });
|
|
await expect(canvas).toBeFocused();
|
|
await expect(canvas).toHaveCSS("outline-style", "none");
|
|
await expect(canvas).toHaveCSS("border-width", "0px");
|
|
await expect(page.locator("#main-workspace")).toHaveCSS("border-width", "0px");
|
|
await page.keyboard.press("Tab");
|
|
await canvas.focus();
|
|
expect(await canvas.evaluate((element) => element.matches(":focus-visible"))).toBe(true);
|
|
await expect(canvas).toHaveCSS("outline-style", "solid");
|
|
await expect(canvas).toHaveCSS("outline-width", "1px");
|
|
await expect(canvas).toHaveCSS("outline-color", await readThemeColor(canvas, "--action-focus-ring"));
|
|
await expect(canvas).toHaveCSS("outline-offset", "-1px");
|
|
await page.screenshot({ path: "test-results/agent-history-ui/map-pointer-focus.png" });
|
|
});
|
|
|
|
test("mobile layer panel keeps Tianditu choices usable", async ({ page }) => {
|
|
await page.setViewportSize({ width: 375, height: 812 });
|
|
await openWorkbench(page);
|
|
await selectBasemap(page, "天地图矢量");
|
|
|
|
await expect(page.locator("main")).toHaveAttribute("data-basemap-tone", "light");
|
|
});
|
|
|
|
test("wide desktop expands the map document when Agent is collapsed", async ({ page }) => {
|
|
await page.setViewportSize({ width: 1920, height: 1080 });
|
|
await openWorkbench(page);
|
|
await openDesktopAgent(page);
|
|
|
|
const agentPanel = page.getByRole("complementary", { name: "Agent 命令面板" });
|
|
const workspace = page.locator("#main-workspace");
|
|
const expandedWorkspace = await workspace.boundingBox();
|
|
expect(expandedWorkspace).not.toBeNull();
|
|
await expect(agentPanel).toHaveCSS("width", "540px");
|
|
|
|
await page.getByRole("button", { name: "折叠 Agent 面板" }).click();
|
|
await expect(page.getByLabel("Agent 折叠栏")).toBeVisible();
|
|
await expect.poll(async () => Math.round((await workspace.boundingBox())?.x ?? 0)).toBe(0);
|
|
await expect
|
|
.poll(async () => Math.round((await workspace.boundingBox())?.width ?? 0))
|
|
.toBe(1920);
|
|
expect((await workspace.boundingBox())!.width).toBeGreaterThan(expandedWorkspace!.width);
|
|
await page.screenshot({ path: "test-results/palette/workbench-desktop-wide-max-agent.png", animations: "disabled" });
|
|
await expect(page).toHaveScreenshot("workbench-desktop-wide-max-agent.png", {
|
|
animations: "disabled",
|
|
maxDiffPixelRatio: 0.01
|
|
});
|
|
});
|
|
|
|
test("mobile launcher keeps Agent, map tools, and conditions visually balanced", async ({
|
|
page
|
|
}) => {
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await openWorkbench(page);
|
|
|
|
await page.screenshot({ path: "test-results/palette/workbench-mobile-launcher-light.png", animations: "disabled" });
|
|
await expect(page).toHaveScreenshot("workbench-mobile-launcher-light.png", {
|
|
animations: "disabled",
|
|
maxDiffPixelRatio: 0.01
|
|
});
|
|
});
|
|
|
|
test("mobile light basemap shows the Agent half sheet", async ({ page }) => {
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await openWorkbench(page);
|
|
await page.getByRole("button", { name: "打开 Agent 面板" }).click();
|
|
|
|
const sheet = page.getByRole("region", { name: "Agent 工作台抽屉" });
|
|
await expect(sheet).toHaveAttribute("data-snap", "half");
|
|
await expect.poll(async () => Math.round((await sheet.boundingBox())?.height ?? 0)).toBe(439);
|
|
await page.screenshot({ path: "test-results/palette/workbench-mobile-agent-light.png", animations: "disabled" });
|
|
await expect(page).toHaveScreenshot("workbench-mobile-agent-light.png", {
|
|
animations: "disabled",
|
|
maxDiffPixelRatio: 0.01
|
|
});
|
|
});
|
|
|
|
test("mobile satellite basemap shows the condition half sheet", async ({ page }) => {
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await openWorkbench(page);
|
|
await selectBasemap(page, "影像");
|
|
await page.getByRole("button", { name: "打开工况任务" }).click();
|
|
|
|
const sheet = page.getByRole("region", { name: "工况任务抽屉" });
|
|
await expect(sheet).toHaveAttribute("data-snap", "half");
|
|
await expect(page.locator("main")).toHaveAttribute("data-basemap-tone", "satellite");
|
|
await page.screenshot({ path: "test-results/palette/workbench-mobile-condition-satellite.png", animations: "disabled" });
|
|
await expect(page).toHaveScreenshot("workbench-mobile-condition-satellite.png", {
|
|
animations: "disabled",
|
|
maxDiffPixelRatio: 0.01
|
|
});
|
|
});
|
|
});
|
|
|
|
async function prepareDeterministicWorkbench(page: Page) {
|
|
await mockAgentApi(page, {
|
|
model: {
|
|
id: "deepseek/deepseek-v4-flash",
|
|
label: "快速",
|
|
description: "快速响应",
|
|
icon: "bolt"
|
|
}
|
|
});
|
|
await page.route("**/runtime-config.js", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/javascript",
|
|
body: `globalThis.__TJWATER_CONFIG__ = {
|
|
TJWATER_AUTH_MODE: "disabled",
|
|
TJWATER_MAPBOX_ACCESS_TOKEN: "visual-test-token",
|
|
TJWATER_TIANDITU_TOKEN: "visual-test-tianditu-token",
|
|
TJWATER_MAP_URL: "https://visual-map.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: "false",
|
|
TJWATER_ENABLE_MSW: "false"
|
|
};`
|
|
});
|
|
});
|
|
await page.route("https://api.mapbox.com/**", async (route) => {
|
|
await route.fulfill({ contentType: "image/png", body: EMPTY_RASTER_TILE });
|
|
});
|
|
await page.route("https://t0.tianditu.gov.cn/**", async (route) => {
|
|
await route.fulfill({ contentType: "image/png", body: EMPTY_RASTER_TILE });
|
|
});
|
|
await page.route("https://visual-map.invalid/**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/vnd.mapbox-vector-tile",
|
|
body: Buffer.alloc(0)
|
|
});
|
|
});
|
|
await page.route("https://demotiles.maplibre.org/**", async (route) => {
|
|
await route.fulfill({ status: 204, body: "" });
|
|
});
|
|
await page.route("**/*.riv", async (route) => {
|
|
await route.abort();
|
|
});
|
|
await mockScadaApi(page);
|
|
}
|
|
|
|
async function openWorkbench(page: Page) {
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
const workbench = page.locator("main[data-basemap-tone]");
|
|
await workbench.waitFor();
|
|
await workbench.evaluate((element) => {
|
|
element.setAttribute("data-visual-test", "true");
|
|
});
|
|
await page.addStyleTag({
|
|
content: `
|
|
[data-sonner-toaster] { display: none !important; }
|
|
[data-visual-test] .maplibregl-canvas { opacity: 0.28; }
|
|
[data-visual-test][data-basemap-tone="light"] .map-grid {
|
|
background:
|
|
radial-gradient(circle at 70% 32%, rgba(56, 120, 170, 0.18) 0 3px, transparent 4px),
|
|
linear-gradient(118deg, transparent 0 46%, rgba(65, 121, 160, 0.12) 46.2% 46.7%, transparent 47%),
|
|
linear-gradient(90deg, rgba(75, 124, 160, 0.06) 1px, transparent 1px),
|
|
linear-gradient(rgba(75, 124, 160, 0.06) 1px, transparent 1px),
|
|
#dce8ef;
|
|
background-size: auto, auto, 48px 48px, 48px 48px, auto;
|
|
}
|
|
[data-visual-test][data-basemap-tone="satellite"] .map-grid {
|
|
background:
|
|
radial-gradient(circle at 68% 30%, rgba(183, 207, 159, 0.24) 0 4px, transparent 5px),
|
|
linear-gradient(126deg, transparent 0 44%, rgba(178, 195, 153, 0.2) 44.2% 44.8%, transparent 45%),
|
|
repeating-linear-gradient(24deg, rgba(34, 67, 58, 0.18) 0 1px, transparent 1px 22px),
|
|
#526961;
|
|
}
|
|
`
|
|
});
|
|
await page.evaluate(async () => {
|
|
await document.fonts.ready;
|
|
});
|
|
await page.waitForFunction(() =>
|
|
Boolean(window.__waterNetworkMap?.getLayer("scada-pressure-hit"))
|
|
);
|
|
await expect(page.getByRole("button", {
|
|
name: "查看待办与告警,当前 0 条待处理事项"
|
|
})).toBeVisible();
|
|
}
|
|
|
|
type BasemapLabel = "浅色" | "影像" | "天地图矢量" | "天地图影像";
|
|
|
|
async function selectBasemap(page: Page, label: BasemapLabel) {
|
|
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
|
|
await page.getByRole("button", { name: label, exact: true }).click();
|
|
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
|
|
await expect(page.locator("main")).toHaveAttribute(
|
|
"data-basemap-tone",
|
|
["影像", "天地图影像"].includes(label) ? "satellite" : "light"
|
|
);
|
|
}
|
|
|
|
async function expectBaseLayerVisibility(page: Page, expected: Record<string, "visible" | "none">) {
|
|
await expect
|
|
.poll(async () =>
|
|
page.evaluate((layerIds) => {
|
|
const map = window.__waterNetworkMap;
|
|
return Object.fromEntries(
|
|
layerIds.map((layerId) => [
|
|
layerId,
|
|
map?.getLayoutProperty(layerId, "visibility") ?? "visible"
|
|
])
|
|
);
|
|
}, Object.keys(expected))
|
|
)
|
|
.toEqual(expected);
|
|
}
|
|
|
|
async function openDesktopAgent(page: Page) {
|
|
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
|
|
await expect(page.getByRole("complementary", { name: "Agent 命令面板" })).toBeVisible();
|
|
}
|
|
|
|
async function expectDesktopMainFrameGeometry(page: Page) {
|
|
const workspace = page.locator("#main-workspace");
|
|
const mapWindow = page.locator("#workspace-map");
|
|
const agentPanel = page.getByRole("complementary", { name: "Agent 命令面板" });
|
|
await expect(agentPanel).toHaveCSS("width", "500px");
|
|
await expect(agentPanel).toHaveCSS("border-radius", "0px");
|
|
await expect(agentPanel).toHaveCSS("border-right-width", "1px");
|
|
await expect(mapWindow).toHaveCSS("border-radius", "0px");
|
|
|
|
const workspaceBox = await workspace.boundingBox();
|
|
const mapBox = await mapWindow.boundingBox();
|
|
const agentBox = await agentPanel.boundingBox();
|
|
expect(workspaceBox).not.toBeNull();
|
|
expect(mapBox).not.toBeNull();
|
|
expect(agentBox).not.toBeNull();
|
|
expect(agentBox!.x).toBe(0);
|
|
expect(agentBox!.y).toBe(56);
|
|
expect(workspaceBox!.x).toBe(agentBox!.x + agentBox!.width);
|
|
expect(workspaceBox!.y).toBe(0);
|
|
expect(workspaceBox!.height).toBe(agentBox!.height + agentBox!.y);
|
|
expect(mapBox).toEqual(workspaceBox);
|
|
}
|
|
|
|
async function expectSolidMainFrameMaterials(page: Page) {
|
|
const materials = await page.evaluate(() => {
|
|
const agent = getComputedStyle(document.querySelector('aside[aria-label="Agent 命令面板"]')!);
|
|
return {
|
|
agentBackdrop: agent.backdropFilter,
|
|
agentBackground: agent.backgroundColor,
|
|
agentShadow: agent.boxShadow
|
|
};
|
|
});
|
|
expect(materials.agentBackdrop).toBe("none");
|
|
expect(materials.agentBackground).toBe(
|
|
await readThemeColor(page.getByRole("complementary", { name: "Agent 命令面板" }), "--agent-dock-surface")
|
|
);
|
|
expect(materials.agentShadow).toBe("none");
|
|
}
|
|
|
|
async function expectLoadedChineseFont(page: Page) {
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => document.fonts.check('16px "Noto Sans SC Variable"', "供水管网"))
|
|
)
|
|
.toBe(true);
|
|
}
|
|
|
|
async function expectBodyContrast(page: Page) {
|
|
const body = page.getByRole("group", { name: "推荐问题" }).getByRole("button").first();
|
|
await expect(body).toBeVisible();
|
|
expect(await readRenderedContrast(body)).toBeGreaterThanOrEqual(4.5);
|
|
}
|