Files
next-tjwater-frontend/tests/browser/workbench-visual.e2e.ts
T
jiang 85a9a3ab45 fix(ui): refine acrylic controls and timeline interactions
Restore full acrylic materials to stacked task cards and floating controls.
Extend the map behind the navigation header while preserving document and
control insets. Align timeline and scale readouts, refresh dense-marker
hover content, and highlight model and approval menu options.

Keep narrow timeline headers clear of their legend and actions. Update
coupled geometry checks and retain city zoom-12 and menu regression coverage.

Validation: scoped git diff --check and read-only UI geometry inspection.
Test suites were not rerun at the user's request.
2026-10-09 18:11:33 +08:00

337 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_API_BASE_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"))
);
}
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.getByText("直接说出你想调查的问题,我会整理监测与空间证据,并将分析结果带回地图。");
expect(await readRenderedContrast(body)).toBeGreaterThanOrEqual(4.5);
}