feat(workbench): integrate operational workflows and map state

This commit is contained in:
2026-09-24 04:34:46 +08:00
parent 1752b9ef84
commit 436d5500f1
76 changed files with 5081 additions and 1937 deletions
+168 -6
View File
@@ -1,4 +1,5 @@
import { expect, test } from "@playwright/test";
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(
@@ -6,13 +7,71 @@ const EMPTY_RASTER_TILE = Buffer.from(
"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(60000);
test.setTimeout(90000);
await mockWorkbenchContext(page, undefined, undefined, {}, [], {
tiandituToken: "city-scene-tianditu-token"
});
@@ -61,6 +120,46 @@ test("3D adds opaque buildings without enabling duplicate GIS layers", async ({
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
@@ -79,16 +178,29 @@ test("3D adds opaque buildings without enabling duplicate GIS layers", async ({
"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).toBeLessThan(
scene.layerOrder.findIndex((layerId) => layerId.startsWith("pipes-"))
);
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(
@@ -97,10 +209,24 @@ test("3D adds opaque buildings without enabling duplicate GIS layers", async ({
)
).toBe(layer.visibility);
}
await page.evaluate(() => window.__waterNetworkMap?.rotateTo(90, { duration: 0 }));
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(() =>
@@ -112,8 +238,44 @@ test("3D adds opaque buildings without enabling duplicate GIS layers", async ({
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"
);
});