Compare commits

..
Author SHA1 Message Date
jiang 209ee8c2ea fix(map): clamp layer fade progress to the valid range
Generic Container CI/CD / test-build-publish (push) Successful in 1m12s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m12s
A frame timestamp may precede performance.now sampled when hiding starts, producing negative progress and opacity above 1. Bound progress at both ends; cover the exact label failure and real MapLibre paint validation.
2026-11-12 13:49:36 +08:00
3 changed files with 40 additions and 2 deletions
@@ -82,6 +82,21 @@ describe("map layer visibility fades", () => {
expect(frames.size).toBe(0); expect(frames.size).toBe(0);
}); });
it("keeps opacity in range when the first frame timestamp precedes the fade start", () => {
const fixture = createMap();
now = 100;
setMapLayersVisibility(fixture.map, ["simulation-backend-analysis-label", "pipes"], false);
// RAF reports the frame start, which can precede performance.now() sampled
// later in that frame when a visibility change begins.
advance(75.8);
expect(fixture.paint.get("simulation-backend-analysis-label:text-opacity")).toBe(1);
expect(fixture.paint.get("pipes:line-opacity")).toBe(0.92);
advance(260);
expect(fixture.paint.get("simulation-backend-analysis-label:text-opacity")).toBe(0.5);
advance(420);
expect(fixture.visibility.get("simulation-backend-analysis-label")).toBe("none");
});
it("starts fading in after vector tiles are ready", () => { it("starts fading in after vector tiles are ready", () => {
const fixture = createMap(); const fixture = createMap();
let loaded = false; let loaded = false;
@@ -102,7 +117,8 @@ describe("map layer visibility fades", () => {
}); });
function createMap() { function createMap() {
const layers = new Map([ ["pipes", { type: "line", source: "pipes" }], ["pipes-hit", { type: "line", source: "pipes" }] ]); const layers = new Map([ ["pipes", { type: "line", source: "pipes" }], ["pipes-hit", { type: "line", source: "pipes" }],
["simulation-backend-analysis-label", { type: "symbol", source: "simulation-backend-analysis" }] ]);
const visibility = new Map([ ["pipes", "visible"], ["pipes-hit", "visible"] ]); const visibility = new Map([ ["pipes", "visible"], ["pipes-hit", "visible"] ]);
const paint = new Map<string, unknown>([ const paint = new Map<string, unknown>([
["pipes:line-opacity", 0.92], ["pipes:line-opacity-transition", { duration: 120 }], ["pipes:line-opacity", 0.92], ["pipes:line-opacity-transition", { duration: 120 }],
@@ -110,7 +110,8 @@ export function setMapLayersVisibility(
return; return;
} }
startedAt ??= now; startedAt ??= now;
const progress = Math.min(1, (now - startedAt) / LAYER_VISIBILITY_FADE_MS); // RAF's frame timestamp may precede performance.now() at the fade start.
const progress = Math.max(0, Math.min(1, (now - startedAt) / LAYER_VISIBILITY_FADE_MS));
fade.opacity = startOpacity + (targetOpacity - startOpacity) * progress; fade.opacity = startOpacity + (targetOpacity - startOpacity) * progress;
if (progress >= 1) finish(); if (progress >= 1) finish();
else { else {
@@ -48,6 +48,27 @@ async function openLayerFixture(page: Page, vectorPipes = false) {
await page.getByRole("button", { name: /图层:管理地图图层/ }).click(); await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
} }
test("analysis label fade accepts a frame timestamp earlier than the visibility change", async ({ page }) => {
await openLayerFixture(page);
await page.evaluate(async () => {
const modulePath = "/src/features/workbench/map/layer-visibility.ts";
const { setMapLayersVisibility } = await import(modulePath);
const map = window.__waterNetworkMap!;
const errors: string[] = [];
map.on("error", (event) => {
if (event.error.message.includes("simulation-backend-analysis-label")) errors.push(event.error.message);
});
Object.assign(window, { analysisFadeErrors: errors });
const nativeFrame = window.requestAnimationFrame.bind(window);
window.requestAnimationFrame = (callback) => nativeFrame((time) => callback(time - 100));
try { setMapLayersVisibility(map, ["simulation-backend-analysis-label"], false); }
finally { window.requestAnimationFrame = nativeFrame; }
});
await expect.poll(() => page.evaluate(() => window.__waterNetworkMap!
.getLayoutProperty("simulation-backend-analysis-label", "visibility"))).toBe("none");
expect(await page.evaluate(() => (window as unknown as { analysisFadeErrors: string[] }).analysisFadeErrors)).toEqual([]);
});
for (const [label, layerId, property] of [ for (const [label, layerId, property] of [
["管线", "pipes-flow", "line-opacity"], ["管线", "pipes-flow", "line-opacity"],
["水库", "reservoirs-symbol", "icon-opacity"], ["水库", "reservoirs-symbol", "icon-opacity"],