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

401 lines
14 KiB
TypeScript
Raw Permalink 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();
});
})
);
}
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);
});
test("city building ranges persist across a page reload", async ({ page }) => {
test.skip(
process.env.TJWATER_CITY_SCENE_TEST !== "1",
"Requires the generated local city-model scene"
);
test.setTimeout(90_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 })
);
const buildingRequests: string[] = [];
page.on("request", (request) => {
if (new URL(request.url()).pathname === "/city-model/buildings.pmtiles") {
buildingRequests.push(request.headers().range ?? "");
}
});
const openScene = async () => {
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: 14,
pitch: 55,
bearing: 0
})
);
await expect
.poll(
() =>
page.evaluate(
() =>
window.__waterNetworkMap?.queryRenderedFeatures({
layers: ["fengxian-city-buildings"]
}).length ?? 0
),
{ timeout: 30_000 }
)
.toBeGreaterThan(0);
await waitForMapIdle(page);
};
await openScene();
expect(buildingRequests.length).toBeGreaterThan(0);
expect(buildingRequests.every((range) => /^bytes=\d+-\d+$/.test(range))).toBe(true);
const cachedRangeCount = await page.evaluate(
() =>
new Promise<number>((resolve, reject) => {
const request = indexedDB.open("tjwater-city-pmtiles-cache");
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const database = request.result;
const transaction = database.transaction("ranges", "readonly");
const countRequest = transaction.objectStore("ranges").count();
countRequest.onerror = () => reject(countRequest.error);
countRequest.onsuccess = () => resolve(countRequest.result);
};
})
);
expect(cachedRangeCount).toBeGreaterThan(0);
buildingRequests.length = 0;
await openScene();
expect(buildingRequests).toEqual([]);
});
// 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"
);
});