164 lines
8.7 KiB
TypeScript
164 lines
8.7 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
||
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
||
import { mockPipeVectorTiles } from "./support/mock-pipe-vector-tiles";
|
||
|
||
declare global {
|
||
interface Window { __layerFadeSamples: number[]; __layerFadePixels: number[]; }
|
||
}
|
||
|
||
async function openLayerFixture(page: Page, vectorPipes = false) {
|
||
await mockWorkbenchContext(page);
|
||
if (vectorPipes) await mockPipeVectorTiles(page);
|
||
await page.route("https://workbench-map.invalid/geoserver/tjwater_next/ows?**", (route) => {
|
||
const type = new URL(route.request().url()).searchParams.get("typeNames");
|
||
return route.fulfill({ contentType: "application/xml",
|
||
body: `<wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs/2.0" numberMatched="${type?.endsWith(":reservoirs") || type?.endsWith(":water_quality_monitoring_points") ? 1 : 0}"/>` });
|
||
});
|
||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||
await expect.poll(() => page.evaluate(() => Boolean(
|
||
window.__waterNetworkMap?.getLayer("reservoirs-symbol")
|
||
)), { timeout: 15_000 }).toBe(true);
|
||
await page.evaluate((vectorPipes) => {
|
||
const map = window.__waterNetworkMap!;
|
||
for (const sourceId of ["pipes", "reservoirs", "water_quality_monitoring_points"]) {
|
||
if (vectorPipes && sourceId === "pipes") continue;
|
||
const layers = map.getStyle().layers.filter((layer) => "source" in layer && layer.source === sourceId);
|
||
[...layers].reverse().forEach((layer) => map.removeLayer(layer.id));
|
||
map.removeSource(sourceId);
|
||
map.addSource(sourceId, { type: "geojson", data: { type: "FeatureCollection", features: [{
|
||
type: "Feature", properties: { id: "fade-fixture", diameter: 800 },
|
||
geometry: sourceId === "pipes"
|
||
? { type: "LineString", coordinates: [[121.45, 30.91], [121.48, 30.91]] }
|
||
: { type: "Point", coordinates: [sourceId === "reservoirs" ? 121.46 : 121.47, 30.915] }
|
||
}] } });
|
||
layers.forEach((layer) => {
|
||
const copy = { ...layer };
|
||
if ("source-layer" in copy) delete copy["source-layer"];
|
||
map.addLayer(copy);
|
||
});
|
||
}
|
||
const scada = map.getSource("scada");
|
||
if (scada && "setData" in scada) scada.setData({ type: "FeatureCollection", features: [{
|
||
type: "Feature", id: "pressure-size-fixture",
|
||
properties: { device_id: "pressure-size-fixture", device_type: "pressure" },
|
||
geometry: { type: "Point", coordinates: [121.475, 30.915] }
|
||
}] });
|
||
map.jumpTo({ center: [121.465, 30.913], zoom: 13, pitch: 0, bearing: 0 });
|
||
}, vectorPipes);
|
||
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
|
||
}
|
||
|
||
for (const [label, layerId, property] of [
|
||
["管线", "pipes-flow", "line-opacity"],
|
||
["水库", "reservoirs-symbol", "icon-opacity"],
|
||
["水质监测点", "water_quality_monitoring_points-symbol", "icon-opacity"]
|
||
] as const) {
|
||
test(`${label} fades on toggle and restores its original paint`, async ({ page }, testInfo) => {
|
||
await openLayerFixture(page);
|
||
const original = await page.evaluate(({ layerId, property }) => {
|
||
const map = window.__waterNetworkMap!;
|
||
const original = map.getPaintProperty(layerId, property);
|
||
const setPaint = map.setPaintProperty.bind(map);
|
||
window.__layerFadeSamples = [];
|
||
map.setPaintProperty = (id, name, value, options) => {
|
||
if (id === layerId && name === property) {
|
||
if (typeof value === "number" && typeof original === "number") {
|
||
window.__layerFadeSamples.push(value / original);
|
||
}
|
||
const output = Array.isArray(value) && value[0] === "interpolate" ? value[4] : value;
|
||
if (Array.isArray(output) && output[0] === "*") {
|
||
window.__layerFadeSamples.push(Number(output.at(-1)));
|
||
}
|
||
}
|
||
return setPaint(id, name, value, options);
|
||
};
|
||
return original;
|
||
}, { layerId, property });
|
||
const toggle = page.getByRole("button", { name: new RegExp(`^${label},`) });
|
||
await toggle.click();
|
||
await expect.poll(() => page.evaluate((id) => window.__waterNetworkMap!
|
||
.getLayoutProperty(id, "visibility"), layerId)).toBe("none");
|
||
const hiddenSamples = await page.evaluate(() => window.__layerFadeSamples);
|
||
expect(hiddenSamples.some((value) => value > 0 && value < 1)).toBe(true);
|
||
expect(await page.evaluate(({ layerId, property }) => window.__waterNetworkMap!
|
||
.getPaintProperty(layerId, property), { layerId, property })).toEqual(original);
|
||
await toggle.click();
|
||
await expect.poll(() => page.evaluate(({ layerId, property }) => window.__waterNetworkMap!
|
||
.getPaintProperty(layerId, property), { layerId, property })).toEqual(original);
|
||
await page.screenshot({ path: testInfo.outputPath("layer-visible.png") });
|
||
await toggle.click();
|
||
await toggle.click();
|
||
await expect.poll(() => page.evaluate((id) => window.__waterNetworkMap!
|
||
.getLayoutProperty(id, "visibility"), layerId)).toBe("visible");
|
||
await expect.poll(() => page.evaluate(({ layerId, property }) => window.__waterNetworkMap!
|
||
.getPaintProperty(layerId, property), { layerId, property })).toEqual(original);
|
||
});
|
||
}
|
||
|
||
test("reduced motion switches layers without a fade", async ({ page }) => {
|
||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||
await openLayerFixture(page);
|
||
await page.getByRole("button", { name: /^管线,/ }).click();
|
||
expect(await page.evaluate(() => window.__waterNetworkMap!
|
||
.getLayoutProperty("pipes-flow", "visibility"))).toBe("none");
|
||
});
|
||
|
||
for (const [label, layerId, property, longitude, latitude] of [
|
||
["管线", "pipes-flow", "line-opacity", 121.465, 30.91],
|
||
["水库", "reservoirs-symbol", "icon-opacity", 121.46, 30.915],
|
||
["水质监测点", "water_quality_monitoring_points-symbol", "icon-opacity", 121.47, 30.915]
|
||
] as const) {
|
||
test(`${label} has intermediate rendered pixels while fading in and out`, async ({ page }, testInfo) => {
|
||
await openLayerFixture(page, label === "管线");
|
||
await expect.poll(() => page.evaluate((id) => window.__waterNetworkMap!
|
||
.queryRenderedFeatures({ layers: [id] }).length, layerId)).toBeGreaterThan(0);
|
||
await page.evaluate(() => new Promise<void>((resolve) => {
|
||
const map = window.__waterNetworkMap!;
|
||
map.once("idle", () => resolve());
|
||
map.triggerRepaint();
|
||
}));
|
||
await page.screenshot({ path: testInfo.outputPath("fade-before.png") });
|
||
const original = await page.evaluate(({ layerId, property, longitude, latitude }) => {
|
||
const map = window.__waterNetworkMap!;
|
||
const canvas = map.getCanvas();
|
||
const gl = canvas.getContext("webgl2") ?? canvas.getContext("webgl");
|
||
if (!gl) throw new Error("No WebGL context");
|
||
window.__layerFadePixels = [];
|
||
map.on("render", () => {
|
||
const point = map.project([longitude, latitude]);
|
||
const ratio = canvas.width / canvas.clientWidth;
|
||
const pixels = new Uint8Array(7 * 7 * 4);
|
||
gl.readPixels(Math.round(point.x * ratio) - 3,
|
||
canvas.height - Math.round(point.y * ratio) - 3, 7, 7, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||
let darkest = 0;
|
||
for (let index = 0; index < pixels.length; index += 4) {
|
||
darkest = Math.max(darkest, 765 - pixels[index] - pixels[index + 1] - pixels[index + 2]);
|
||
}
|
||
window.__layerFadePixels.push(darkest);
|
||
});
|
||
map.triggerRepaint();
|
||
return map.getPaintProperty(layerId, property);
|
||
}, { layerId, property, longitude, latitude });
|
||
const toggle = page.getByRole("button", { name: new RegExp(`^${label},`) });
|
||
await toggle.click();
|
||
await expect.poll(() => page.evaluate((id) => window.__waterNetworkMap!
|
||
.getLayoutProperty(id, "visibility"), layerId)).toBe("none");
|
||
const hiding = await page.evaluate(() => window.__layerFadePixels);
|
||
const min = Math.min(...hiding);
|
||
const max = Math.max(...hiding);
|
||
await testInfo.attach("hiding-pixels", { body: JSON.stringify(hiding), contentType: "application/json" });
|
||
expect(max - min).toBeGreaterThan(50);
|
||
// Require multiple rendered intermediate alpha states without prescribing a GPU frame rate.
|
||
expect(new Set(hiding.filter((pixel) => pixel > min + 10 && pixel < max - 10)).size).toBeGreaterThan(1);
|
||
await page.evaluate(() => { window.__layerFadePixels = []; });
|
||
await toggle.click();
|
||
await expect.poll(() => page.evaluate(({ layerId, property }) => window.__waterNetworkMap!
|
||
.getPaintProperty(layerId, property), { layerId, property })).toEqual(original);
|
||
const showing = await page.evaluate(() => window.__layerFadePixels);
|
||
expect(new Set(showing.filter((pixel) => pixel > min + 10 && pixel < max - 10)).size).toBeGreaterThan(1);
|
||
await testInfo.attach("rendered-opacity", {
|
||
body: JSON.stringify({ hiding, showing }), contentType: "application/json"
|
||
});
|
||
});
|
||
}
|