feat(charts): manage investigation charts in a scrolling workspace
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
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" });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user