Files
next-tjwater-frontend/tests/browser/layer-visibility-fade.e2e.ts
T

164 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
});
});
}