Files
next-tjwater-frontend/tests/browser/city-scene.e2e.ts
T

282 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<FrameTimeSummary> {
const frameTimes = await page.evaluate(
({ targetBearing }) =>
new Promise<number[]>((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<void>((resolve) => {
const map = window.__waterNetworkMap!;
if (map.loaded()) {
resolve();
return;
}
const timeout = window.setTimeout(resolve, 5_000);
map.once("idle", () => {
window.clearTimeout(timeout);
resolve();
});
})
);
}
// 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"
);
});