import { expect, test, type Locator, type Page } from "@playwright/test"; import { mockAgentApi } from "./support/mock-agent-api"; import { createAnalysisRuntimeSnapshot, MOCK_SPATIAL_ANALYSIS } from "./support/mock-analysis-runtime"; import { mockScadaApi } from "./support/mock-scada-api"; test.beforeEach(async ({ page }) => { await page.emulateMedia({ reducedMotion: "reduce" }); await prepareWorkbench(page); }); test("assigned desktop panels avoid native form controls and horizontal overflow", async ({ page }) => { await page.setViewportSize({ width: 1440, height: 900 }); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.evaluate(() => document.fonts.ready); await expect(page.locator("[data-sonner-toast]")).toHaveCount(0, { timeout: 15_000 }); const mapToolbar = page.getByRole("navigation", { name: "地图工具" }); await page.getByRole("button", { name: "工况任务", exact: true }).click(); const conditionPanel = page.getByRole("region", { name: "工况任务", exact: true }); await expect(conditionPanel).toBeVisible(); const [toolbarBox, conditionBox] = await Promise.all([ mapToolbar.boundingBox(), conditionPanel.boundingBox() ]); expect(toolbarBox).not.toBeNull(); expect(conditionBox).not.toBeNull(); expect(Math.abs(toolbarBox!.y - conditionBox!.y)).toBeLessThanOrEqual(1); await page.getByRole("button", { name: "工况任务", exact: true }).click(); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]'); await expectContained(agentPanel, 1440); await page.getByRole("button", { name: "打开 Agent 历史记录" }).click(); await expectContained(page.getByRole("region", { name: "Agent 历史记录" }), 1440); await page.setViewportSize({ width: 1280, height: 900 }); await page.getByRole("button", { name: /图层:管理地图图层/ }).click(); const layerPanel = page.getByRole("region", { name: "图层" }); await expectContained(layerPanel, 1280); await expect(layerPanel.getByText("监测点", { exact: true })).toBeVisible(); await expect(layerPanel.getByRole("button", { name: /压力监测点/ })).toBeVisible(); await expect(layerPanel.getByRole("button", { name: /流量监测点/ })).toBeVisible(); await expect(layerPanel.getByRole("button", { name: /水质监测点/ })).toBeVisible(); const sourceGisGroup = layerPanel.locator("details").filter({ hasText: "GIS 原始数据" }); await expect(sourceGisGroup).not.toHaveAttribute("open", ""); await sourceGisGroup.locator("summary").click(); await expect(sourceGisGroup).toHaveAttribute("open", ""); await expect(sourceGisGroup.getByText("2020-04-23 · 全网除头桥快照")).toBeVisible(); await expect(sourceGisGroup.getByText("2020-08-11 · 专项管线台账")).toBeVisible(); await expect(sourceGisGroup.getByText("1 要素", { exact: false }).first()).toBeVisible(); await sourceGisGroup.getByRole("button", { name: /GIS 管线/ }).click(); await sourceGisGroup.getByRole("button", { name: /DN100\+ 台账/ }).click(); for (const facilityLabel of [ "GIS 阀门", "排水阀", "流量仪", "消火栓", "接水点", "用户水表", "非居民水表" ]) { await sourceGisGroup.getByRole("button", { name: new RegExp(facilityLabel) }).click(); } await expect(sourceGisGroup).toHaveAttribute("open", ""); await page.screenshot({ path: "test-results/non-map-panels/1280-layer-panel.png", fullPage: true }); await page.getByRole("button", { name: /图例:查看当前地图符号/ }).click(); const legendPanel = page.getByRole("region", { name: "图例" }); await expectContained(legendPanel, 1280); await expect(legendPanel.getByRole("region", { name: "模型管线" })).toBeVisible(); await expect(legendPanel.getByRole("region", { name: "供水设施" })).toBeVisible(); await expect(legendPanel.getByRole("region", { name: "监测点" })).toBeVisible(); const aprilSourcePipeLegend = legendPanel.getByRole("region", { name: "原始 GIS 管线 · 2020-04-23" }); await expect(aprilSourcePipeLegend).toBeVisible(); await expect(aprilSourcePipeLegend.getByText("DN600 及以上", { exact: true })).toBeVisible(); await expect(aprilSourcePipeLegend.getByText("DN300-DN599", { exact: true })).toBeVisible(); await expect(aprilSourcePipeLegend.getByText("DN300 以下", { exact: true })).toBeVisible(); await expect(aprilSourcePipeLegend.getByText("未标注口径", { exact: true })).toBeVisible(); const augustSourcePipeLegend = legendPanel.getByRole("region", { name: "原始 GIS 管线 · 2020-08-11" }); await expect(augustSourcePipeLegend).toBeVisible(); await expect(augustSourcePipeLegend.getByText("DN100 及以上", { exact: true })).toBeVisible(); const sourceFacilityLegend = legendPanel.getByRole("region", { name: "原始 GIS 设施" }); await expect(sourceFacilityLegend).toBeVisible(); await expect(sourceFacilityLegend.locator('img[src^="/map/gis-"]')).toHaveCount(7); await expect(legendPanel.getByRole("region", { name: "分析覆盖" })).toHaveCount(0); await expect(legendPanel.locator(".status-dot")).toHaveCount(0); await expect(legendPanel.locator('svg line[stroke-width="5"]')).not.toHaveCount(0); await expect(legendPanel.locator('svg line[stroke-width="2"]')).not.toHaveCount(0); await page.screenshot({ path: "test-results/non-map-panels/1280-legend-panel.png", fullPage: true }); await sourceFacilityLegend.scrollIntoViewIfNeeded(); await page.screenshot({ path: "test-results/non-map-panels/1280-legend-source-gis.png", fullPage: true }); await page.getByRole("button", { name: /测量:距离与范围测量/ }).click(); const measurePanel = page.getByRole("region", { name: "测量", exact: true }); await expectContained(measurePanel, 1280); await expect(measurePanel).toHaveCSS("width", "324px"); const meterUnit = measurePanel.getByRole("button", { name: "m", exact: true }); const kilometerUnit = measurePanel.getByRole("button", { name: "km", exact: true }); await expect(kilometerUnit).toHaveAttribute("aria-pressed", "true"); await expect(kilometerUnit).toHaveClass(/bg-blue-600/); await meterUnit.click(); await expect(meterUnit).toHaveAttribute("aria-pressed", "true"); await expect(meterUnit).toHaveClass(/bg-blue-600/); await kilometerUnit.click(); const distanceMode = measurePanel.getByRole("button", { name: "距离", exact: true }); const areaMode = measurePanel.getByRole("button", { name: "面积", exact: true }); const segmentMode = measurePanel.getByRole("button", { name: "管段", exact: true }); await expect(distanceMode).toHaveAttribute("aria-pressed", "true"); await areaMode.focus(); await expect(areaMode).toBeFocused(); await page.keyboard.press("Enter"); await expect(areaMode).toHaveAttribute("aria-pressed", "true"); await expect(measurePanel.getByText("当前面积", { exact: true })).toBeVisible(); await segmentMode.click(); await expect(segmentMode).toHaveAttribute("aria-pressed", "true"); await expect(measurePanel.getByText("资产长度", { exact: true })).toBeVisible(); await distanceMode.click(); await expect(measurePanel.getByRole("button", { name: "复制结果" })).toBeDisabled(); await expect(measurePanel.getByRole("button", { name: "清除测量" })).toBeDisabled(); await expect(measurePanel.getByText("等待开始", { exact: true })).toBeVisible(); const startMeasureButton = measurePanel.getByRole("button", { name: /开始测量/ }); await expect(startMeasureButton).toHaveClass(/bg-slate-50\/90/); await expect(startMeasureButton).not.toHaveClass(/bg-blue-600/); await expect(startMeasureButton).toContainText("在地图中连续取点"); await startMeasureButton.click(); const stopMeasureButton = measurePanel.getByRole("button", { name: /停止测量/ }); await expect(stopMeasureButton).toBeVisible(); await expect(stopMeasureButton).toHaveAttribute("data-state", "active"); await expect(stopMeasureButton).toHaveClass(/border-blue-300/); await expect(stopMeasureButton).toContainText("暂停并保留当前读数"); await expect(stopMeasureButton).toContainText("进行中"); await expect(measurePanel.getByText("测量中", { exact: true })).toBeVisible(); await page.screenshot({ path: "test-results/non-map-panels/1280-measure-panel-active.png", fullPage: true }); await stopMeasureButton.click(); const resumedStartMeasureButton = measurePanel.getByRole("button", { name: /开始测量/ }); await expect(resumedStartMeasureButton).toBeEnabled(); await expect(resumedStartMeasureButton).toHaveClass(/bg-slate-50\/90/); await expect(resumedStartMeasureButton).not.toHaveClass(/bg-blue-600/); await page.screenshot({ path: "test-results/non-map-panels/1280-measure-panel.png", fullPage: true }); await page.getByRole("button", { name: /标注:管理地图标注/ }).click(); await expectContained(page.getByRole("region", { name: "标注" }), 1280); await page.screenshot({ path: "test-results/non-map-panels/1280-tool-panels.png", fullPage: true }); await page.getByRole("button", { name: "切换地图 Dev Panel" }).click(); const devPanel = page.getByRole("complementary", { name: "地图开发面板" }); await expectContained(devPanel, 1280); await expect(devPanel.locator("select")).toHaveCount(0); const layerGroup = devPanel.getByRole("combobox", { name: "业务图层组" }); await layerGroup.focus(); await layerGroup.press("Enter"); const junctionOption = page.getByRole("option", { name: "节点" }); await expect(junctionOption).toBeVisible(); await junctionOption.focus(); await page.keyboard.press("Enter"); await expect(layerGroup).toHaveText("节点"); await expect( devPanel.locator('input:not([type="range"]):not([type="color"]):not([type="checkbox"])').first() ).toHaveCSS("font-family", /Noto Sans SC/); await page.screenshot({ path: "test-results/non-map-panels/1280-dev-panel.png", fullPage: true }); }); for (const width of [375, 320]) { test(`mobile Agent and tool panels fit ${width}px without horizontal overflow`, async ({ page }) => { await page.setViewportSize({ width, height: 844 }); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.evaluate(() => document.fonts.ready); await expect(page.locator("[data-sonner-toast]")).toHaveCount(0, { timeout: 15_000 }); await page.getByRole("button", { name: "打开 Agent 面板" }).click(); const agentSheet = page.getByRole("region", { name: "Agent 工作台抽屉" }); await expectContained(agentSheet, width); const welcomeTitle = agentSheet.getByRole("heading", { name: "我已就绪,请描述任务" }); await expect(welcomeTitle).toBeVisible(); const [welcomeBox, conversationBox] = await Promise.all([ welcomeTitle.boundingBox(), agentSheet.getByRole("log").boundingBox() ]); expect(welcomeBox).not.toBeNull(); expect(conversationBox).not.toBeNull(); expect(welcomeBox!.y).toBeGreaterThanOrEqual(conversationBox!.y); expect(welcomeBox!.y + welcomeBox!.height).toBeLessThanOrEqual( conversationBox!.y + conversationBox!.height ); await expect(page.locator("[data-sonner-toast]")).toHaveCount(0, { timeout: 15_000 }); await page.screenshot({ path: `test-results/non-map-panels/${width}-agent.png`, fullPage: true }); await page.getByRole("button", { name: "关闭 Agent 面板" }).click(); await page.getByRole("button", { name: "更多:更多地图操作" }).click(); const toolPanel = page.getByRole("region", { name: "更多" }); await expectContained(toolPanel, width); await page.screenshot({ path: `test-results/non-map-panels/${width}-more.png`, fullPage: true }); await page.getByRole("button", { name: /图层:管理地图图层/ }).click(); const mobileLayerPanel = page.getByRole("region", { name: "图层" }); await expectContained(mobileLayerPanel, width); await expect(mobileLayerPanel.getByRole("button", { name: /管线,按管径分级/ })).toBeVisible(); await expect(mobileLayerPanel.getByText("GIS 原始数据", { exact: true })).toBeAttached(); await page.screenshot({ path: `test-results/non-map-panels/${width}-layers.png`, fullPage: true }); await page.getByRole("button", { name: /图例:查看当前地图符号/ }).click(); const mobileLegendPanel = page.getByRole("region", { name: "图例" }); await expectContained(mobileLegendPanel, width); await expect(mobileLegendPanel.getByRole("region", { name: "模型管线" })).toBeVisible(); await expect(mobileLegendPanel.getByRole("region", { name: "供水设施" })).toBeAttached(); await expect(mobileLegendPanel.getByRole("region", { name: "监测点" })).toBeAttached(); await expect(mobileLegendPanel.getByRole("region", { name: /原始 GIS 管线/ })).toHaveCount(0); await page.screenshot({ path: `test-results/non-map-panels/${width}-legend.png`, fullPage: true }); await page.getByRole("button", { name: /测量:距离与范围测量/ }).click(); const mobileMeasurePanel = page.getByRole("region", { name: "测量", exact: true }); await expectContained(mobileMeasurePanel, width); await expect(mobileMeasurePanel.getByRole("group", { name: "测量类型" })).toBeVisible(); await expect(mobileMeasurePanel.getByRole("button", { name: "复制结果" })).toBeDisabled(); await expect(mobileMeasurePanel.getByRole("button", { name: "清除测量" })).toBeDisabled(); await mobileMeasurePanel.getByRole("button", { name: "面积", exact: true }).click(); await expect(mobileMeasurePanel.getByText("当前面积", { exact: true })).toBeVisible(); await mobileMeasurePanel.getByRole("button", { name: /开始测量/ }).click(); await expect(mobileMeasurePanel.getByText("测量中", { exact: true })).toBeVisible(); await expect(mobileMeasurePanel.getByRole("button", { name: /停止测量/ })).toContainText("进行中"); await mobileMeasurePanel.getByRole("button", { name: /停止测量/ }).click(); await page.screenshot({ path: `test-results/non-map-panels/${width}-measure.png`, fullPage: true }); if (width === 375) { await page.getByRole("button", { name: "打开工况任务" }).click(); const conditionSheet = page.getByRole("region", { name: "工况任务抽屉" }); await expectContained(conditionSheet, width); await page.screenshot({ path: "test-results/non-map-panels/375-condition-feed.png", fullPage: true }); await conditionSheet .getByRole("button", { name: /SCADA 数据诊断/ }) .first() .click(); await expectContained(conditionSheet, width); await page.screenshot({ path: "test-results/non-map-panels/375-condition-detail.png", fullPage: true }); } }); } for (const width of [1280, 375]) { test(`artifact preview and document remain contained at ${width}px`, async ({ page }) => { await page.setViewportSize({ width: Math.max(width, 1280), height: 900 }); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.evaluate(() => document.fonts.ready); await expect(page.locator("[data-sonner-toast]")).toHaveCount(0, { timeout: 15_000 }); await page.getByRole("button", { name: "打开最新后端分析成果" }).click(); await page.getByRole("button", { name: "将成果收起为预览" }).click(); await page.setViewportSize({ width, height: 900 }); const preview = page.getByRole("complementary", { name: "后端分析成果预览" }); await expectContained(preview, width); await page.screenshot({ path: `test-results/non-map-panels/${width}-artifact-preview.png`, fullPage: true }); await preview.getByRole("button", { name: "打开成果" }).click(); const workspace = page.locator("#artifact-workspace"); await expectContained(workspace, width); const heading = workspace.getByRole("heading", { name: "北区爆管定位" }); await expect(heading).toBeVisible(); await expect(heading).toHaveCSS("font-family", /Noto Sans SC/); if (width === 1280) { const [railBox, headingBox] = await Promise.all([ page.getByRole("complementary", { name: "Agent 折叠栏" }).boundingBox(), heading.boundingBox() ]); expect(railBox).not.toBeNull(); expect(headingBox).not.toBeNull(); expect(headingBox!.x).toBeGreaterThanOrEqual(railBox!.x + railBox!.width); } await expect(page.locator("[data-sonner-toast]")).toHaveCount(0, { timeout: 15_000 }); await page.screenshot({ path: `test-results/non-map-panels/${width}-artifact-document.png`, fullPage: true }); }); } async function expectContained(locator: Locator, viewportWidth: number) { await expect(locator).toBeVisible(); const box = await locator.boundingBox(); expect(box).not.toBeNull(); expect(box!.x).toBeGreaterThanOrEqual(0); expect(box!.x + box!.width).toBeLessThanOrEqual(viewportWidth + 1); expect(await locator.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe( true ); } async function prepareWorkbench(page: Page) { await mockAgentApi(page); await page.route("**/runtime-config.js", (route) => route.fulfill({ contentType: "application/javascript", body: `globalThis.__TJWATER_CONFIG__=${JSON.stringify({ TJWATER_AUTH_MODE: "disabled", TJWATER_MAP_URL: "https://non-map-panels.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: "true", TJWATER_ENABLE_MSW: "false" })}` }) ); await page.route("https://non-map-panels.invalid/**", (route) => { const url = new URL(route.request().url()); if (url.searchParams.get("resultType") === "hits") { return route.fulfill({ contentType: "application/xml", body: '' }); } return route.fulfill({ status: 204 }); }); await page.route("https://demotiles.maplibre.org/**", (route) => route.fulfill({ status: 204 })); await page.route("**/*.riv", (route) => route.abort()); await mockScadaApi(page, [], createAnalysisRuntimeSnapshot(MOCK_SPATIAL_ANALYSIS)); }