401 lines
14 KiB
TypeScript
401 lines
14 KiB
TypeScript
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"
|
||
);
|
||
});
|