diff --git a/src/features/workbench/map/layer-visibility.test.ts b/src/features/workbench/map/layer-visibility.test.ts index 8949509..6655c6d 100644 --- a/src/features/workbench/map/layer-visibility.test.ts +++ b/src/features/workbench/map/layer-visibility.test.ts @@ -82,6 +82,21 @@ describe("map layer visibility fades", () => { 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", () => { const fixture = createMap(); let loaded = false; @@ -102,7 +117,8 @@ describe("map layer visibility fades", () => { }); 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 paint = new Map([ ["pipes:line-opacity", 0.92], ["pipes:line-opacity-transition", { duration: 120 }], diff --git a/src/features/workbench/map/layer-visibility.ts b/src/features/workbench/map/layer-visibility.ts index defe7d7..44df7ee 100644 --- a/src/features/workbench/map/layer-visibility.ts +++ b/src/features/workbench/map/layer-visibility.ts @@ -110,7 +110,8 @@ export function setMapLayersVisibility( return; } 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; if (progress >= 1) finish(); else { diff --git a/tests/browser/layer-visibility-fade.e2e.ts b/tests/browser/layer-visibility-fade.e2e.ts index 4ed942c..3da75c0 100644 --- a/tests/browser/layer-visibility-fade.e2e.ts +++ b/tests/browser/layer-visibility-fade.e2e.ts @@ -48,6 +48,27 @@ async function openLayerFixture(page: Page, vectorPipes = false) { 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 [ ["管线", "pipes-flow", "line-opacity"], ["水库", "reservoirs-symbol", "icon-opacity"],