Derive the gem-blue palette with OKLCH tonal and complementary-color rules while preserving acrylic opacity, blur and surface hierarchy. Darker focus tokens exposed stacked field rings, fallback outlines and persistent basemap selection rings. Keep fallback focus styles in the base layer and use one themed field boundary or inset card stroke. Preserve keyboard focus, invalid states and native input semantics. Add browser regression coverage for pointer and keyboard focus, all basemap presentations, narrow layouts and rendered contrast. Validation in an isolated checkout: pnpm lint, pnpm typecheck, pnpm test (393 tests), focused browser suites and pnpm build.
387 lines
20 KiB
TypeScript
387 lines
20 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);
|
|
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);
|
|
const welcomeTitle = agentSheet.getByRole("heading", { name: "我已就绪,请描述任务" });
|
|
await expect(welcomeTitle).toBeVisible();
|
|
const [welcomeBox, conversationBox] = await Promise.all([
|
|
welcomeTitle.boundingBox(),
|
|
agentSheet.getByRole("log").boundingBox()
|
|
]);
|
|
expect(welcomeBox).not.toBeNull();
|
|
expect(conversationBox).not.toBeNull();
|
|
expect(welcomeBox!.y).toBeGreaterThanOrEqual(conversationBox!.y);
|
|
expect(welcomeBox!.y + welcomeBox!.height).toBeLessThanOrEqual(
|
|
conversationBox!.y + conversationBox!.height
|
|
);
|
|
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_API_BASE_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));
|
|
}
|