Restore full acrylic materials to stacked task cards and floating controls. Extend the map behind the navigation header while preserving document and control insets. Align timeline and scale readouts, refresh dense-marker hover content, and highlight model and approval menu options. Keep narrow timeline headers clear of their legend and actions. Update coupled geometry checks and retain city zoom-12 and menu regression coverage. Validation: scoped git diff --check and read-only UI geometry inspection. Test suites were not rerun at the user's request.
326 lines
12 KiB
TypeScript
326 lines
12 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);
|
||
});
|
||
|
||
// 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"
|
||
);
|
||
});
|