Files
next-tjwater-frontend/tests/browser/map-option-interaction.e2e.ts
T
jiang 0ae0a41e61
Generic Container CI/CD / test-build-publish (push) Successful in 2m7s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m8s
feat(charts): manage investigation charts in a scrolling workspace
2026-11-10 17:56:23 +08:00

237 lines
15 KiB
TypeScript

import { expect, test, type Locator, type Page } from "@playwright/test";
import { mockInvestigationPresentation } from "./support/mock-investigation-presentation";
test.use({ reducedMotion: "reduce" });
async function themeColor(element: Locator, token: string, property: string) {
return cssColor(element, `var(${token})`, property);
}
async function cssColor(element: Locator, color: string, property: string) {
return element.evaluate((node, { color, property }) => {
const probe = document.createElement("span");
probe.style.setProperty(property, color);
node.append(probe);
const value = getComputedStyle(probe).getPropertyValue(property);
probe.remove();
return value;
}, { color, property });
}
async function expectOrdinaryInteraction(row: Locator, label?: string, description?: string) {
await expect(row).toHaveCSS("background-color", await themeColor(row, "--surface-reading", "background-color"));
await expect(row).toHaveCSS("border-top-color", await themeColor(row, "--surface-border", "border-top-color"));
await expect(row).toHaveCSS("color", await themeColor(row, "--action-blue-hover", "color"));
if (label) await expect(row.getByText(label, { exact: true })).toHaveCSS("color", await themeColor(row, "--action-blue-hover", "color"));
if (await row.locator("svg").count()) await expect(row.locator("svg").first()).toHaveCSS("color", await themeColor(row, "--action-blue-hover", "color"));
if (description) await expect(row.getByText(description, { exact: true })).toHaveCSS("color", await themeColor(row, "--ink-muted", "color"));
}
async function appearance(row: Locator) {
return row.evaluate((node) => {
const style = getComputedStyle(node);
return { background: style.backgroundColor, border: style.borderTopColor, color: style.color };
});
}
async function expectPreservedOnHover(page: Page, row: Locator, selected = false) {
await page.mouse.move(1, 1);
const before = await appearance(row);
await row.scrollIntoViewIfNeeded();
const bounds = (await row.boundingBox())!;
await page.mouse.move(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2);
if (selected) {
await expect(row).toHaveCSS("background-color", before.background);
await expect(row).toHaveCSS("color", before.color);
await expect(row).toHaveCSS("border-top-color", await themeColor(row, "--action-border-hover", "border-top-color"));
} else {
await expect.poll(() => appearance(row)).toEqual(before);
}
}
async function expectContained(row: Locator, width: number) {
const bounds = (await row.boundingBox())!;
expect(bounds.x).toBeGreaterThanOrEqual(0);
expect(bounds.x + bounds.width).toBeLessThanOrEqual(width);
expect(await row.evaluate((node) => node.scrollWidth - node.clientWidth)).toBeLessThanOrEqual(1);
}
for (const width of [1440, 375]) {
test(`map options share hover and keyboard colors while keeping selected and disabled states at ${width}px`, async ({ page }, testInfo) => {
test.setTimeout(90_000);
await page.setViewportSize({ width, height: 900 });
const mock = await mockInvestigationPresentation(page, []);
// Enable an alternate basemap with a synthetic token; every tile stays local.
await page.route("**/runtime-config.js", (route) => route.fulfill({ contentType: "application/javascript", body: `globalThis.__TJWATER_CONFIG__=${JSON.stringify({
TJWATER_AUTH_MODE: "disabled", TJWATER_SERVER_API_BASE_URL: "https://tjwater-api.invalid",
TJWATER_AGENT_URL: "http://127.0.0.1:8787", TJWATER_ENABLE_DEV_PANEL: "true",
TJWATER_MAP_URL: "https://workbench-map.invalid/geoserver", TJWATER_GEOSERVER_WORKSPACE: "tjwater_next",
TJWATER_MAPBOX_ACCESS_TOKEN: "", TJWATER_TIANDITU_TOKEN: "synthetic-browser-test", TJWATER_ENABLE_MSW: "false"
})};` }));
await page.route("https://*.tianditu.gov.cn/**", (route) => route.fulfill({ contentType: "image/png", body: Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgYGBgAAAABQABpfZFQAAAAABJRU5ErkJggg==", "base64") }));
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => Boolean(window.__waterNetworkMap?.getLayer("scada-pressure-hit")));
await page.getByRole("button", { name: "更多:更多地图操作", exact: true }).click();
const more = page.getByRole("region", { name: "更多", exact: true });
const refresh = more.getByRole("button", { name: /刷新瓦片/ });
const exportView = more.getByRole("button", { name: /当前分辨率/ });
const restingRefresh = await appearance(refresh);
const restingSibling = await appearance(exportView);
await refresh.hover();
await expectOrdinaryInteraction(refresh, "刷新瓦片", "请求地图重新渲染");
expect(await appearance(exportView)).toEqual(restingSibling);
await exportView.hover();
await expect.poll(() => appearance(refresh)).toEqual(restingRefresh);
await expectOrdinaryInteraction(exportView, "当前分辨率", "按当前地图画布尺寸导出");
await refresh.hover();
await expectContained(more, width);
await testInfo.attach(`more-hover-${width}`, { body: await more.screenshot({ path: testInfo.outputPath(`more-hover-${width}.png`) }), contentType: "image/png" });
await page.getByRole("button", { name: "测量:距离与范围测量", exact: true }).click();
const measure = page.getByRole("region", { name: "测量", exact: true });
const distance = measure.getByRole("button", { name: "距离", exact: true });
const area = measure.getByRole("button", { name: "面积", exact: true });
await expect(distance).toHaveAttribute("aria-pressed", "true");
await expectPreservedOnHover(page, distance, true);
await area.hover();
await expectOrdinaryInteraction(area, "面积");
const meters = measure.getByRole("button", { name: "m", exact: true });
const kilometers = measure.getByRole("button", { name: "km", exact: true });
await expectPreservedOnHover(page, kilometers, true);
await meters.hover();
await expectOrdinaryInteraction(meters);
const clear = measure.getByRole("button", { name: "清除测量", exact: true });
await expect(clear).toBeDisabled();
await expectPreservedOnHover(page, clear);
const start = measure.getByRole("button", { name: /开始测量/ });
await start.hover();
await expect(start).toHaveCSS("background-color", await themeColor(start, "--action-blue-hover", "background-color"));
await expect(start).toHaveCSS("color", "rgb(255, 255, 255)");
await expect(start.getByText("开始测量", { exact: true })).toHaveCSS("color", "rgb(255, 255, 255)");
await expect(start.locator("svg").first()).toHaveCSS("color", "rgb(255, 255, 255)");
await expect(start.getByText("在地图中连续取点", { exact: true })).toHaveCSS("color", await cssColor(start, "color-mix(in oklab, white 85%, transparent)", "color"));
await start.click();
for (const close of await page.getByRole("button", { name: "关闭通知", exact: true }).all()) {
if (await close.isVisible()) await close.click();
}
const canvas = page.locator("canvas.maplibregl-canvas");
await canvas.click({ position: { x: 100, y: 175 }, timeout: 10_000 });
await canvas.click({ position: { x: 150, y: 205 }, timeout: 10_000 });
const copy = measure.getByRole("button", { name: "复制结果", exact: true });
await expect(copy).toBeEnabled();
await copy.hover();
await expectOrdinaryInteraction(copy, "复制结果");
await expect(clear).toBeEnabled();
await clear.hover();
await expect(clear).toHaveCSS("background-color", await themeColor(clear, "--status-danger-soft", "background-color"));
await expect(clear).toHaveCSS("border-top-color", await themeColor(clear, "--status-danger-border", "border-top-color"));
await expect(clear).toHaveCSS("color", await themeColor(clear, "--status-danger-foreground", "color"));
await expect(clear.locator("svg").first()).toHaveCSS("color", await themeColor(clear, "--status-danger-foreground", "color"));
await expectContained(measure, width);
await testInfo.attach(`measure-hover-${width}`, { body: await measure.screenshot({ path: testInfo.outputPath(`measure-hover-${width}.png`) }), contentType: "image/png" });
await page.getByRole("button", { name: "图层:管理地图图层", exact: true }).click();
const layers = page.getByRole("region", { name: "图层", exact: true });
const noBasemap = layers.getByRole("button", { name: "无底图", exact: true });
await noBasemap.scrollIntoViewIfNeeded();
await noBasemap.hover();
await expectOrdinaryInteraction(noBasemap, "无底图");
const vector = layers.getByRole("button", { name: "天地图矢量", exact: true });
await expect(vector).toHaveAttribute("aria-pressed", "true");
await expectPreservedOnHover(page, vector, true);
const light = layers.getByRole("button", { name: "浅色", exact: true });
await expect(light).toBeDisabled();
await expectPreservedOnHover(page, light);
await expectContained(layers, width);
await testInfo.attach(`basemap-hover-${width}`, { body: await layers.screenshot({ path: testInfo.outputPath(`basemap-hover-${width}.png`) }), contentType: "image/png" });
await page.getByRole("button", { name: "图层:管理地图图层", exact: true }).click();
const header = page.locator(".workbench-top-bar");
const user = header.getByRole("button", { name: "打开用户菜单", exact: true });
await user.click();
const status = page.getByRole("menuitem", { name: /查看运行状态/ });
await status.hover();
await expectOrdinaryInteraction(status, "查看运行状态", "检查数据源与运行健康度");
const logout = page.getByRole("menuitem", { name: "退出登录", exact: true });
await expect(logout).toHaveAttribute("aria-disabled", "true");
await expectPreservedOnHover(page, logout);
const userMenu = page.getByRole("menu");
await expectContained(userMenu, width);
await page.keyboard.press("Escape");
await expect(userMenu).toHaveCount(0);
await page.mouse.move(1, 1);
await user.focus();
await expect(user).toBeFocused();
await user.press("ArrowDown");
await expect(userMenu).toBeVisible();
const menuItems = userMenu.getByRole("menuitem");
for (let index = 0; index < await menuItems.count(); index++) {
await expect(menuItems.nth(index)).toBeFocused();
if (await status.evaluate((node) => document.activeElement === node)) break;
await page.keyboard.press("ArrowDown");
}
await expect(status).toBeFocused();
await expect(status).toHaveAttribute("data-highlighted", "");
await expectOrdinaryInteraction(status, "查看运行状态", "检查数据源与运行健康度");
await expect(status).toHaveCSS("outline-width", "1px");
await expect(status).toHaveCSS("outline-offset", "-1px");
await testInfo.attach(`header-keyboard-${width}`, { body: await userMenu.screenshot({ path: testInfo.outputPath(`header-keyboard-${width}.png`) }), contentType: "image/png" });
await page.keyboard.press("Escape");
await expect(userMenu).toHaveCount(0);
// Dev tools are a desktop entry; verify the resulting window menu at the target width.
if (width < 800) await page.setViewportSize({ width: 1440, height: 900 });
await page.getByRole("button", { name: "切换地图 Dev Panel", exact: true }).click();
const tools = page.getByTestId("investigation-dev-tools");
await tools.getByLabel("调查展示 JSONL", { exact: true }).fill(JSON.stringify({ record_type: "presentation", command: {
action: "open_chart", chart_id: "hover-chart", evidence_refs: ["dev:synthetic"], chart: {
title: "交互验证压力趋势", chart_type: "line", x_data: ["08:00", "08:15"], series: [{ name: "压力", data: [30, 28] }]
}
} }));
await expect(tools.getByRole("combobox", { name: "JSONL 测试案例", exact: true })).toBeEnabled({ timeout: 15_000 });
await tools.getByRole("button", { name: "执行本地预览", exact: true }).click();
await expect(tools.getByRole("status")).toContainText("本地预览完成");
await page.getByLabel("本地预览展示", { exact: true }).getByRole("button", { name: "打开图表管理", exact: true }).click();
await expect(page.getByRole("dialog", { name: "图表管理", exact: true })).toBeVisible();
await page.setViewportSize({ width, height: 900 });
const navigator = header.getByRole("button", { name: /窗口管理,当前/ });
await navigator.click();
const windowMenu = page.getByRole("menu", { name: "已打开的窗口", exact: true });
const currentWindow = windowMenu.getByRole("menuitem", { name: /定位窗口 .*图表管理/ });
await expect(currentWindow).toHaveAttribute("aria-current", "true");
await expectPreservedOnHover(page, currentWindow, true);
await expect(currentWindow).toHaveCSS("background-color", await themeColor(currentWindow, "--action-selection-soft", "background-color"));
await expect(currentWindow).toHaveCSS("color", await themeColor(currentWindow, "--action-blue-hover", "color"));
const closeWindows = windowMenu.getByRole("menuitem", { name: "关闭全部窗口", exact: true });
await closeWindows.hover();
await expectOrdinaryInteraction(closeWindows);
await expectContained(windowMenu, width);
await expectContained(header, width);
expect(await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)).toBeLessThanOrEqual(1);
await testInfo.attach(`window-hover-${width}`, { body: await windowMenu.screenshot({ path: testInfo.outputPath(`window-hover-${width}.png`) }), contentType: "image/png" });
expect(mock.mutations).toEqual([]);
});
}
test.describe("touch map options", () => {
test.use({ hasTouch: true, isMobile: true, viewport: { width: 375, height: 812 } });
test("touch taps keep ordinary rows free of sticky hover colors", async ({ page }, testInfo) => {
await mockInvestigationPresentation(page, []);
await page.goto("/", { waitUntil: "domcontentloaded" });
expect(await page.evaluate(() => matchMedia("(hover: hover)").matches)).toBe(false);
await page.getByRole("button", { name: "更多:更多地图操作", exact: true }).tap();
const more = page.getByRole("region", { name: "更多", exact: true });
const refresh = more.getByRole("button", { name: /刷新瓦片/ });
await refresh.tap({ trial: true });
const before = await appearance(refresh);
const bounds = await refresh.boundingBox();
await refresh.tap();
await expect.poll(() => appearance(refresh)).toEqual(before);
expect(await refresh.boundingBox()).toEqual(bounds);
await expect(refresh).not.toHaveCSS("color", await themeColor(refresh, "--action-blue-hover", "color"));
await expectContained(more, 375);
await testInfo.attach("touch-options-375", { body: await more.screenshot({ path: testInfo.outputPath("touch-options-375.png") }), contentType: "image/png" });
});
});