import { writeFile } from "node:fs/promises"; import { expect, test, type Page } from "@playwright/test"; import { mockWorkbenchContext } from "./support/mock-workbench-context"; const EMPTY_RASTER_TILE = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", "base64" ); type FrameTimeSummary = { frames: number; median_frame_ms: number; p95_frame_ms: number; slow_frame_ratio: number; }; async function measureFramePerformance(page: Page, bearing?: number): Promise { const frameTimes = await page.evaluate( ({ targetBearing }) => new Promise((resolve) => { const samples: number[] = []; const startedAt = performance.now(); let previousFrame = startedAt; const sample = (now: number) => { samples.push(now - previousFrame); previousFrame = now; if (now - startedAt >= 1_800) { resolve(samples.slice(1)); return; } requestAnimationFrame(sample); }; requestAnimationFrame(sample); if (targetBearing !== null) { window.__waterNetworkMap!.easeTo({ bearing: targetBearing, duration: 1_400 }); } }), { targetBearing: bearing ?? null } ); const sorted = [...frameTimes].sort((left, right) => left - right); const percentile = (value: number) => sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * value))]; return { frames: sorted.length, median_frame_ms: percentile(0.5), p95_frame_ms: percentile(0.95), slow_frame_ratio: sorted.filter((frameTime) => frameTime > 33.4).length / sorted.length }; } async function waitForMapIdle(page: Page) { await page.evaluate( () => new Promise((resolve) => { const map = window.__waterNetworkMap!; if (map.loaded()) { resolve(); return; } const timeout = window.setTimeout(resolve, 5_000); map.once("idle", () => { window.clearTimeout(timeout); resolve(); }); }) ); } test("city buildings render only from zoom 12 in 3D", async ({ page }) => { test.skip( process.env.TJWATER_CITY_SCENE_TEST !== "1", "Requires the generated local city-model scene" ); test.setTimeout(60_000); await mockWorkbenchContext(page, undefined, undefined, {}, [], { tiandituToken: "city-scene-tianditu-token" }); await page.route("https://t0.tianditu.gov.cn/**", (route) => route.fulfill({ contentType: "image/png", body: EMPTY_RASTER_TILE }) ); await page.goto("/", { waitUntil: "domcontentloaded" }); const toggle = page .getByRole("group", { name: "地图缩放", exact: true }) .locator("button[aria-pressed]"); await expect(toggle).toBeEnabled({ timeout: 15_000 }); await toggle.click(); await page.evaluate(() => window.__waterNetworkMap!.jumpTo({ center: [121.464, 30.916], zoom: 12, pitch: 55, bearing: 0 }) ); const renderedBuildings = () => page.evaluate(() => { const map = window.__waterNetworkMap!; return map.getLayer("fengxian-city-buildings") ? map.queryRenderedFeatures({ layers: ["fengxian-city-buildings"] }).length : 0; }); await expect.poll(renderedBuildings, { timeout: 30_000 }).toBeGreaterThan(0); await page.screenshot({ path: "test-results/city-model/zoom-12.png" }); for (const zoom of [11.99, 11]) { await page.evaluate((value) => window.__waterNetworkMap!.jumpTo({ zoom: value }), zoom); await expect.poll(renderedBuildings).toBe(0); } await page.screenshot({ path: "test-results/city-model/zoom-11.png" }); for (const zoom of [12, 15, 16]) { await page.evaluate((value) => window.__waterNetworkMap!.jumpTo({ zoom: value }), zoom); await expect.poll(renderedBuildings, { timeout: 15_000 }).toBeGreaterThan(0); } await toggle.click(); await expect.poll(() => page.evaluate(() => window.__waterNetworkMap!.getPitch())).toBe(0); await expect.poll(renderedBuildings).toBe(0); }); // Uses generated local building data, while unrelated backend services are isolated. test("3D adds opaque buildings without enabling duplicate GIS layers", async ({ page }) => { test.skip( process.env.TJWATER_CITY_SCENE_TEST !== "1", "Requires the generated local city-model scene" ); test.setTimeout(90000); await mockWorkbenchContext(page, undefined, undefined, {}, [], { tiandituToken: "city-scene-tianditu-token" }); await page.route("https://t0.tianditu.gov.cn/**", (route) => route.fulfill({ contentType: "image/png", body: EMPTY_RASTER_TILE }) ); await page.goto("/", { waitUntil: "domcontentloaded" }); const toggle = page .getByRole("group", { name: "地图缩放", exact: true }) .locator("button[aria-pressed]"); await expect(toggle).toBeEnabled({ timeout: 15000 }); const businessLayers = await page.evaluate(() => window.__waterNetworkMap!.getStyle().layers.map((layer) => ({ id: layer.id, visibility: window.__waterNetworkMap!.getLayoutProperty(layer.id, "visibility") ?? "visible" })) ); await toggle.click(); await expect(toggle).toHaveAttribute("aria-pressed", "true"); await page.evaluate(() => window.__waterNetworkMap?.jumpTo({ center: [121.464, 30.916], zoom: 16, pitch: 55 }) ); await expect .poll( () => page.evaluate( () => window.__waterNetworkMap?.queryRenderedFeatures({ layers: ["fengxian-city-buildings"] }) .length ?? 0 ), { timeout: 30000 } ) .toBeGreaterThan(20); const buildingPoint = await page.evaluate(() => { const map = window.__waterNetworkMap!; const canvas = map.getCanvas(); for (let y = 20; y < canvas.clientHeight; y += 20) { for (let x = 20; x < canvas.clientWidth; x += 20) { if (map.queryRenderedFeatures([x, y], { layers: ["fengxian-city-buildings"] }).length > 0) { return { x, y }; } } } return null; }); expect(buildingPoint).not.toBeNull(); const mapCanvasBounds = await page.locator(".maplibregl-canvas").boundingBox(); expect(mapCanvasBounds).not.toBeNull(); await page.evaluate(({ x, y }) => { const map = window.__waterNetworkMap!; const location = map.unproject([x, y]); const source = map.getSource("scada") as { setData: (data: unknown) => void }; source.setData({ type: "FeatureCollection", features: [ { type: "Feature", geometry: { type: "Point", coordinates: [location.lng, location.lat] }, properties: { device_id: "OCCLUDED-SCADA-001", device_type: "pressure", name: "建筑后方测试点", status: "online", value: 0.31, unit: "MPa" } } ] }); }, buildingPoint!); await expect .poll(() => page.evaluate( ({ x, y }) => window.__waterNetworkMap!.queryRenderedFeatures([x, y], { layers: ["scada-pressure-hit"] }).length, buildingPoint! ) ) .toBeGreaterThan(0); await page.mouse.move( mapCanvasBounds!.x + buildingPoint!.x, mapCanvasBounds!.y + buildingPoint!.y ); await expect .poll(() => page.locator(".maplibregl-canvas").evaluate((canvas) => canvas.style.cursor)) .toBe(""); await page.mouse.click( mapCanvasBounds!.x + buildingPoint!.x, mapCanvasBounds!.y + buildingPoint!.y ); await expect(page.locator(".maplibregl-popup")).toHaveCount(0); await expect(page.getByText("建筑参考模型", { exact: true })).toHaveCount(0); const scene = await page.evaluate(() => ({ layers: window .__waterNetworkMap!.getStyle() .layers.filter((layer) => layer.id.startsWith("fengxian-city-")) .map((layer) => layer.id), sources: Object.keys(window.__waterNetworkMap!.getStyle().sources).filter((id) => id.startsWith("fengxian-city-") ), opacity: window.__waterNetworkMap!.getPaintProperty( "fengxian-city-buildings", "fill-extrusion-opacity" ), light: window.__waterNetworkMap!.getLight(), antialias: window .__waterNetworkMap! .getCanvas() .getContext("webgl2") ?.getContextAttributes()?.antialias, layerOrder: window.__waterNetworkMap!.getStyle().layers.map((layer) => layer.id) })); expect(scene.layers).toEqual(["fengxian-city-buildings"]); expect(scene.sources).toEqual(["fengxian-city-buildings"]); expect(scene.opacity).toBe(1); expect(scene.light).toMatchObject({ anchor: "map", position: [1.5, 225, 35], color: "#EAF2F4", intensity: 0.42 }); expect(scene.antialias).toBe(true); const buildingIndex = scene.layerOrder.indexOf("fengxian-city-buildings"); expect(buildingIndex).toBeGreaterThan(scene.layerOrder.indexOf("tianditu-vector-annotation")); expect(buildingIndex).toBeGreaterThan(scene.layerOrder.indexOf("pipes-flow")); expect(buildingIndex).toBeGreaterThan(scene.layerOrder.indexOf("pipes-hover")); expect(buildingIndex).toBeLessThan(scene.layerOrder.indexOf("pipes-hit")); for (const layer of businessLayers) { expect( await page.evaluate( (id) => window.__waterNetworkMap!.getLayoutProperty(id, "visibility") ?? "visible", layer.id ) ).toBe(layer.visibility); } await waitForMapIdle(page); const desktopStationaryPerformance = await measureFramePerformance(page); const desktopRotationPerformance = await measureFramePerformance(page, 90); expect(desktopStationaryPerformance.frames).toBeGreaterThan(5); expect(desktopRotationPerformance.frames).toBeGreaterThan(5); await expect(page.locator("[data-map-compass]")).toHaveAttribute("data-bearing", "90.0"); await page.evaluate(() => window.__waterNetworkMap?.rotateTo(0, { duration: 0 })); await page.screenshot({ path: "../city-model-fengxian/reports/browser-nanqiao.png" }); const downloadPromise = page.waitForEvent("download"); const moreButton = page.getByRole("button", { name: "更多:更多地图操作", exact: true }); await moreButton.click(); await page.getByRole("button", { name: /^当前分辨率 / }).click(); const download = await downloadPromise; expect(download.suggestedFilename()).toMatch(/^supply-network-view-1\.0x-/); await download.saveAs("../city-model-fengxian/reports/browser-export.png"); await expect(page.getByText(/已导出 \d+ × \d+ PNG。/)).toBeVisible(); await moreButton.click(); await expect(moreButton).toHaveAttribute("aria-pressed", "false"); await toggle.click(); await expect .poll(() => page.evaluate(() => window.__waterNetworkMap?.getLayoutProperty("fengxian-city-buildings", "visibility") ) ) .toBe("none"); await page.setViewportSize({ width: 390, height: 844 }); await toggle.click(); await expect(toggle).toHaveAttribute("aria-pressed", "true"); await expect .poll( () => page.evaluate( () => window.__waterNetworkMap?.queryRenderedFeatures({ layers: ["fengxian-city-buildings"] }) .length ?? 0 ), { timeout: 30000 } ) .toBeGreaterThan(0); await waitForMapIdle(page); const mobileStationaryPerformance = await measureFramePerformance(page); const mobileRotationPerformance = await measureFramePerformance(page, -45); expect(mobileStationaryPerformance.frames).toBeGreaterThan(5); expect(mobileRotationPerformance.frames).toBeGreaterThan(5); const box = await toggle.boundingBox(); expect(box?.width).toBe(40); expect(box?.height).toBe(40); await page.screenshot({ path: "../city-model-fengxian/reports/browser-mobile.png" }); await writeFile( "../city-model-fengxian/reports/browser-performance.json", `${JSON.stringify( { median_frame_ms: desktopRotationPerformance.median_frame_ms, p95_frame_ms: desktopRotationPerformance.p95_frame_ms, samples: { desktop_stationary: desktopStationaryPerformance, desktop_rotation: desktopRotationPerformance, mobile_stationary: mobileStationaryPerformance, mobile_rotation: mobileRotationPerformance }, environment: "Playwright Chromium on this workstation; stationary and rotating desktop/mobile 3D views with MSAA" }, null, 2 )}\n`, "utf8" ); });