Files
next-tjwater-frontend/tests/browser/button-system.e2e.ts
T
jiang f9c71cc076
Generic Container CI/CD / test-build-publish (push) Successful in 26s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 26s
feat(workbench): align v2 GIS and SCADA API
2026-09-08 18:18:40 +08:00

236 lines
9.5 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import { mockScadaApi } from "./support/mock-scada-api";
test.beforeEach(async ({ page }) => {
await prepareWorkbench(page);
});
test("desktop controls share focus, hover, press and open states", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto("/", { waitUntil: "domcontentloaded" });
const scenario = page.getByRole("button", { name: "切换模拟方案" });
await expect(scenario).toBeVisible();
await expect.poll(async () => (await scenario.boundingBox())?.height).toBe(32);
const compactHitArea = await scenario.evaluate(
(element) => getComputedStyle(element, "::after").inset
);
expect(compactHitArea).toBe("-4px");
await scenario.focus();
await expect
.poll(async () => scenario.evaluate((element) => getComputedStyle(element).boxShadow))
.not.toBe("none");
await scenario.hover();
await expect
.poll(async () => scenario.evaluate((element) => getComputedStyle(element).backgroundColor))
.not.toBe("rgba(0, 0, 0, 0)");
const pressTarget = page.getByRole("button", { name: /任务浮条/ });
const restingBackground = await pressTarget.evaluate(
(element) => getComputedStyle(element).backgroundColor
);
expect(restingBackground).toBe("rgb(232, 241, 255)");
await pressTarget.hover();
const box = await pressTarget.boundingBox();
expect(box).not.toBeNull();
await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2);
await page.mouse.down();
await expect
.poll(async () => pressTarget.evaluate((element) => element.matches(":active")))
.toBe(true);
await expect
.poll(async () => pressTarget.evaluate((element) => getComputedStyle(element).filter))
.toBe("none");
const pressedBox = await pressTarget.boundingBox();
expect(pressedBox).toEqual(box);
await expect
.poll(async () => pressTarget.evaluate((element) => getComputedStyle(element).backgroundColor))
.toBe("rgb(207, 226, 255)");
await page.mouse.move(720, 460);
await page.mouse.up();
await expect
.poll(async () => pressTarget.evaluate((element) => getComputedStyle(element).filter))
.toBe("none");
await expect(pressTarget).toHaveAttribute("aria-pressed", "true");
await pressTarget.hover();
const hoverBackground = await pressTarget.evaluate((element) => {
const probe = document.createElement("span");
probe.style.backgroundColor = "var(--action-selection-soft-hover)";
element.append(probe);
const backgroundColor = getComputedStyle(probe).backgroundColor;
probe.remove();
return backgroundColor;
});
await expect
.poll(async () => pressTarget.evaluate((element) => getComputedStyle(element).backgroundColor))
.toBe(hoverBackground);
await page.mouse.move(720, 460);
await expect
.poll(async () => pressTarget.evaluate((element) => getComputedStyle(element).backgroundColor))
.toBe(restingBackground);
await page.getByRole("button", { name: "打开用户菜单" }).click();
const userButton = page.getByRole("button", { name: "打开用户菜单" });
await expect(userButton).toHaveAttribute("data-state", "open");
await expect
.poll(async () => userButton.evaluate((element) => getComputedStyle(element).backgroundColor))
.not.toBe("rgba(0, 0, 0, 0)");
const mapTool = page.getByRole("button", { name: /图层:管理地图图层/ });
const mapToolBox = await mapTool.boundingBox();
expect(mapToolBox?.width).toBe(40);
expect(mapToolBox?.height).toBe(40);
});
test("mobile launchers and map dock keep non-overlapping touch targets", async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await page.goto("/", { waitUntil: "domcontentloaded" });
const agentLauncher = page.getByRole("button", { name: "打开 Agent 面板" });
const conditionLauncher = page.getByRole("button", { name: "打开工况任务" });
const mapTools = page.locator('nav[aria-label="地图工具"]:visible button:visible');
for (const locator of [agentLauncher, conditionLauncher]) {
const box = await locator.boundingBox();
expect(box?.width).toBeGreaterThanOrEqual(44);
expect(box?.height).toBeGreaterThanOrEqual(44);
}
const toolCount = await mapTools.count();
expect(toolCount).toBeGreaterThan(0);
for (let index = 0; index < toolCount; index += 1) {
const box = await mapTools.nth(index).boundingBox();
expect(box?.width).toBeGreaterThanOrEqual(40);
expect(box?.height).toBeGreaterThanOrEqual(40);
}
const rectangles = await Promise.all(
[agentLauncher, mapTools.first(), conditionLauncher].map((locator) => locator.boundingBox())
);
expect(rectangles.every(Boolean)).toBe(true);
expect(rectangles[0]!.x + rectangles[0]!.width).toBeLessThanOrEqual(rectangles[1]!.x);
expect(rectangles[1]!.x + rectangles[1]!.width).toBeLessThan(rectangles[2]!.x);
});
test("mobile timeline only offers collapse after opening its detail sheet", async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await page.goto("/", { waitUntil: "domcontentloaded" });
const timelineSummary = page.getByRole("region", { name: "移动端运行时间轴摘要" });
await expect(timelineSummary).toBeVisible();
await expect(timelineSummary.getByRole("button", { name: "隐藏运行时间轴" })).toHaveCount(0);
await timelineSummary.getByRole("button", { name: "打开完整运行时间轴" }).click();
const timelineSheet = page.getByRole("region", { name: "运行时间轴抽屉" });
await expect(timelineSheet).toBeVisible();
await timelineSheet.getByRole("button", { name: "收起时间轴" }).click();
await expect(timelineSheet).toBeHidden();
await expect(timelineSummary).toBeVisible();
});
test("mobile map tool panel stays above the bottom dock", async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: "更多:更多地图操作" }).click();
const toolPanel = page.getByRole("region", { name: "更多" });
const mapDock = page.locator('nav[aria-label="地图工具"]:visible');
await expect(toolPanel).toBeVisible();
await expect
.poll(async () => {
const [panelBox, dockBox] = await Promise.all([
toolPanel.boundingBox(),
mapDock.boundingBox()
]);
if (!panelBox || !dockBox) return Number.POSITIVE_INFINITY;
return Math.round(panelBox.y + panelBox.height - dockBox.y);
})
.toBeLessThanOrEqual(-8);
});
test("mobile map zoom stays above the bottom dock", async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await page.goto("/", { waitUntil: "domcontentloaded" });
const mapZoom = page.getByRole("group", { name: "地图缩放" });
const mapDock = page.locator('nav[aria-label="地图工具"]:visible');
await expect(mapZoom).toBeVisible();
await expect
.poll(async () => {
const [zoomBox, dockBox] = await Promise.all([
mapZoom.boundingBox(),
mapDock.boundingBox()
]);
if (!zoomBox || !dockBox) return Number.POSITIVE_INFINITY;
return Math.round(zoomBox.y + zoomBox.height - dockBox.y);
})
.toBeLessThanOrEqual(-8);
const zoomBox = await mapZoom.boundingBox();
expect(Math.round(375 - zoomBox!.x - zoomBox!.width)).toBe(12);
});
test("Agent content buttons do not gain a border while pressed", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.evaluate(() => {
const conversation = document.createElement("div");
conversation.className = "agent-panel-conversation";
Object.assign(conversation.style, {
position: "fixed",
inset: "100px auto auto 100px",
zIndex: "9999"
});
const button = document.createElement("button");
button.type = "button";
button.textContent = "测试内容按钮";
Object.assign(button.style, { width: "120px", height: "40px" });
conversation.append(button);
document.body.append(conversation);
});
const contentButton = page.getByRole("button", { name: "测试内容按钮" });
const box = await contentButton.boundingBox();
expect(box).not.toBeNull();
await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2);
await page.mouse.down();
await expect
.poll(async () => contentButton.evaluate((element) => element.matches(":active")))
.toBe(true);
await expect(contentButton).toHaveCSS("outline-style", "none");
await page.mouse.up();
});
async function prepareWorkbench(page: Page) {
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: "",
TJWATER_MAP_URL: "https://button-system.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://button-system.invalid/**", async (route) => {
await route.fulfill({ status: 204, body: "" });
});
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);
}