236 lines
9.5 KiB
TypeScript
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);
|
|
}
|