feat(workbench): integrate operational workflows and map state
This commit is contained in:
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user