import { expect, test, type Page } from "@playwright/test"; 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("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_MAPBOX_ACCESS_TOKEN: "", TJWATER_MAP_URL: "https://button-system.invalid/geoserver", TJWATER_GEOSERVER_WORKSPACE: "tjwater", 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(); }); }