From 038d3458bcf12d1e1d227e7467813b89d81f1bb5 Mon Sep 17 00:00:00 2001 From: Huarch Date: Mon, 9 Nov 2026 17:59:36 +0800 Subject: [PATCH] feat(map): manage agent overlays in layer panel --- .../map/core/components/layer-panel.tsx | 13 +- .../workbench/data/workbench-simulation.ts | 73 ---------- .../workbench/hooks/use-workbench-map.ts | 32 +---- src/features/workbench/map-workbench-page.tsx | 64 ++++++--- .../workbench/map/annotation-layers.ts | 134 +----------------- .../workbench/map/map-control-config.test.ts | 38 +++-- .../workbench/map/map-control-config.ts | 78 +++++----- tests/browser/analysis-overlay-layers.e2e.ts | 54 +++++++ tests/browser/scada-api-source.e2e.ts | 4 +- .../browser/workbench-runtime-backend.e2e.ts | 5 +- 10 files changed, 174 insertions(+), 321 deletions(-) delete mode 100644 src/features/workbench/data/workbench-simulation.ts create mode 100644 tests/browser/analysis-overlay-layers.e2e.ts diff --git a/src/features/map/core/components/layer-panel.tsx b/src/features/map/core/components/layer-panel.tsx index e0be043..3720e6d 100644 --- a/src/features/map/core/components/layer-panel.tsx +++ b/src/features/map/core/components/layer-panel.tsx @@ -46,17 +46,10 @@ type LayerVisibilitySectionProps = { function LayerVisibilitySection({ items, onToggleLayer }: LayerVisibilitySectionProps) { const groups = groupLayerItems(items); - const [openGroupIds, setOpenGroupIds] = useState( - () => new Set(groups.filter((group) => group.defaultOpen).map((group) => group.id)) - ); + const [groupOpenStates, setGroupOpenStates] = useState>({}); const handleGroupToggle = (groupId: string, open: boolean) => { - setOpenGroupIds((current) => { - const next = new Set(current); - if (open) next.add(groupId); - else next.delete(groupId); - return next; - }); + setGroupOpenStates((current) => ({ ...current, [groupId]: open })); }; return ( @@ -64,7 +57,7 @@ function LayerVisibilitySection({ items, onToggleLayer }: LayerVisibilitySection {groups.map((group) => (
handleGroupToggle(group.id, event.currentTarget.open)} className="surface-well group/layer overflow-hidden rounded-xl border" > diff --git a/src/features/workbench/data/workbench-simulation.ts b/src/features/workbench/data/workbench-simulation.ts deleted file mode 100644 index 43a0165..0000000 --- a/src/features/workbench/data/workbench-simulation.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { MapAnnotation } from "../types"; - -export const mapAnnotations: MapAnnotation[] = [ - { - id: "burst", - label: "演示爆管位置", - coordinate: [121.5215, 30.9084], - kind: "burst" - }, - { - id: "dn600", - label: "演示 DN600", - coordinate: [121.512, 30.9102], - kind: "pipe-label" - }, - { - id: "east-gate", - label: "演示片区", - coordinate: [121.545, 30.951], - kind: "district" - }, - { - id: "impact", - label: "演示影响范围 约 1.82 km²", - coordinate: [121.567, 30.896], - kind: "tooltip" - } -]; - -export const impactAreaPolygon = { - type: "FeatureCollection" as const, - features: [ - { - type: "Feature" as const, - properties: { - id: "impact-area", - label: "演示影响范围", - area: "1.82 km²" - }, - geometry: { - type: "Polygon" as const, - coordinates: [ - [ - [121.5205, 30.9084], - [121.548, 30.928], - [121.604, 30.918], - [121.626, 30.886], - [121.584, 30.862], - [121.538, 30.873], - [121.506, 30.895], - [121.5205, 30.9084] - ] - ] - } - } - ] -}; - -export const annotationPoints = { - type: "FeatureCollection" as const, - features: mapAnnotations.map((annotation) => ({ - type: "Feature" as const, - properties: { - id: annotation.id, - label: annotation.label, - kind: annotation.kind - }, - geometry: { - type: "Point" as const, - coordinates: annotation.coordinate - } - })) -}; diff --git a/src/features/workbench/hooks/use-workbench-map.ts b/src/features/workbench/hooks/use-workbench-map.ts index 9c7c3ae..7d42904 100644 --- a/src/features/workbench/hooks/use-workbench-map.ts +++ b/src/features/workbench/hooks/use-workbench-map.ts @@ -70,7 +70,6 @@ type UseWorkbenchMapOptions = { onSelectFeature: (feature: DetailFeature) => void; selectedFeature: DetailFeature | null; simulationOverlay: FeatureCollection | null; - allowDemoSimulation: boolean; networkView?: NetworkViewParams | null; getAccessToken?: WorkbenchAccess; geoserverWorkspace?: string; @@ -121,8 +120,6 @@ const SOURCE_GROUP_BY_ID: Record = { ...Object.fromEntries( GEOSERVER_WATER_NETWORK_SOURCE_IDS.map((sourceId) => [sourceId, "geoserver-mvt"]) ), - [SIMULATION_SOURCE_IDS.impactArea]: "annotation-source", - [SIMULATION_SOURCE_IDS.annotations]: "annotation-source", [SIMULATION_SOURCE_IDS.backendAnalysis]: "annotation-source" }; @@ -142,7 +139,6 @@ export function useWorkbenchMap({ onSelectFeature, selectedFeature, simulationOverlay, - allowDemoSimulation, networkView, geoserverWorkspace, getAccessToken @@ -150,11 +146,9 @@ export function useWorkbenchMap({ const mapRef = useRef(null); const impactVisibleRef = useRef(impactVisible); const simulationOverlayRef = useRef(simulationOverlay); - const allowDemoSimulationRef = useRef(allowDemoSimulation); const networkViewRef = useRef(networkView ?? WATER_NETWORK_GLOBAL_VIEW); const fittedNetworkViewRef = useRef(null); simulationOverlayRef.current = simulationOverlay; - allowDemoSimulationRef.current = allowDemoSimulation; networkViewRef.current = networkView ?? WATER_NETWORK_GLOBAL_VIEW; const [mapReady, setMapReady] = useState(false); const [mapError, setMapError] = useState(null); @@ -315,10 +309,8 @@ export function useWorkbenchMap({ const source = sources[sourceId]; if (source) map.addSource(sourceId, source); }); - map.addSource(SIMULATION_SOURCE_IDS.impactArea, simulationSources.impactArea); - map.addSource(SIMULATION_SOURCE_IDS.annotations, simulationSources.annotations); map.addSource(SIMULATION_SOURCE_IDS.backendAnalysis, simulationSources.backendAnalysis); - syncSimulationSourceData(map, simulationOverlayRef.current, allowDemoSimulationRef.current); + syncSimulationSourceData(map, simulationOverlayRef.current); await registerSupplyAssetImages(map, scadaAbortController.signal); if (scadaAbortController.signal.aborted) return; const scadaRegistration = await registerScadaImages(map, scadaAbortController.signal); @@ -327,16 +319,13 @@ export function useWorkbenchMap({ scadaRegistration.status === "unavailable" ? scadaFallbackBusinessLayers : scadaBusinessLayers; - simulationAnnotationLayers - .filter((layer) => layer.id === "simulation-impact-fill") - .forEach((layer) => map.addLayer(layer)); filterWaterNetworkLayersBySourceIds( waterNetworkBusinessLayers, REQUIRED_GEOSERVER_SOURCE_IDS ).forEach((layer) => map.addLayer(layer)); activeScadaBusinessLayers.forEach((layer) => map.addLayer(layer)); simulationAnnotationLayers - .filter((layer) => layer.type !== "symbol" && layer.id !== "simulation-impact-fill") + .filter((layer) => layer.type !== "symbol") .forEach((layer) => map.addLayer(layer)); filterWaterNetworkLayersBySourceIds( waterNetworkInteractionLayers, @@ -484,8 +473,8 @@ export function useWorkbenchMap({ useEffect(() => { const map = mapRef.current; if (!map || !mapReady) return; - syncSimulationSourceData(map, simulationOverlay, allowDemoSimulation); - }, [allowDemoSimulation, mapReady, simulationOverlay]); + syncSimulationSourceData(map, simulationOverlay); + }, [mapReady, simulationOverlay]); function fitToNetworkBounds() { const map = mapRef.current; @@ -541,23 +530,12 @@ function getScadaStatusMessage(snapshot: Awaited | null, - allowDemoSimulation: boolean + backendOverlay: FeatureCollection | null ) { const useBackendOverlay = Boolean(backendOverlay?.features.length); (map.getSource(SIMULATION_SOURCE_IDS.backendAnalysis) as GeoJSONSource | undefined)?.setData( useBackendOverlay ? backendOverlay! : EMPTY_SIMULATION_COLLECTION ); - (map.getSource(SIMULATION_SOURCE_IDS.impactArea) as GeoJSONSource | undefined)?.setData( - useBackendOverlay || !allowDemoSimulation - ? EMPTY_SIMULATION_COLLECTION - : simulationSources.impactArea.data - ); - (map.getSource(SIMULATION_SOURCE_IDS.annotations) as GeoJSONSource | undefined)?.setData( - useBackendOverlay || !allowDemoSimulation - ? EMPTY_SIMULATION_COLLECTION - : simulationSources.annotations.data - ); } function syncDataDependentGeoServerLayers( diff --git a/src/features/workbench/map-workbench-page.tsx b/src/features/workbench/map-workbench-page.tsx index a63188c..7a085e9 100644 --- a/src/features/workbench/map-workbench-page.tsx +++ b/src/features/workbench/map-workbench-page.tsx @@ -99,6 +99,7 @@ import { import { getResponsivePadding, getResponsiveWorkbenchPadding } from "./map/camera"; import { exportMapViewImage } from "./map/export-view"; import { setMapLayersVisibility } from "./map/layer-visibility"; +import { MAP_STYLE_TOKENS } from "./map/map-colors"; import { BASE_LAYER_OPTIONS, INITIAL_LAYER_VISIBILITY, @@ -298,9 +299,6 @@ function MapWorkbenchContent({ const artifactActive = artifactWorkspace.activeArtifact !== null; const activeSpatialOverlay = artifactWorkspace.activeArtifact?.spatialOverlay ?? null; const activeArtifactHasSpatialView = Boolean(activeSpatialOverlay?.features.length); - const allowDemoSimulation = - (!getWorkbenchProject(getAccessToken) || getWorkbenchProject(getAccessToken)?.gs_workspace === "tjwater_next") - && (artifactWorkspace.activeArtifact === null || artifactWorkspace.activeArtifact.origin === "demo"); const latestAnalysisAvailable = Boolean( latestAnalysisArtifact?.blocks.some( (block) => @@ -355,11 +353,6 @@ function MapWorkbenchContent({ useEffect(() => { setImpactVisible(activeArtifactHasSpatialView); - setLayerVisibility((current) => - current.simulation === activeArtifactHasSpatialView - ? current - : { ...current, simulation: activeArtifactHasSpatialView } - ); }, [activeArtifactHasSpatialView, artifactWorkspace.activeArtifact?.id]); function openAgentWorkspaceForViewport() { @@ -458,7 +451,6 @@ function MapWorkbenchContent({ onSelectFeature: handleSelectFeature, selectedFeature: detailFeature, simulationOverlay: activeSpatialOverlay, - allowDemoSimulation, networkView: projectPresentation.networkView, geoserverWorkspace: getWorkbenchProject(getAccessToken)?.gs_workspace, getAccessToken @@ -690,23 +682,55 @@ function MapWorkbenchContent({ [activeToolId, mapReady, toggleToolbarTool] ); - const layerControlItems = useMemo( - () => createLayerControlItems(layerVisibility, availableSourceIds, sourceFeatureCounts), - [availableSourceIds, layerVisibility, sourceFeatureCounts] + const overlayLayers = [ + { scope: "investigation", description: "Agent 调查", surface: presentation }, + { scope: "chat", description: "Agent 对话", surface: chatPresentation }, + { scope: "preview", description: "本地预览", surface: previewPresentation } + ].flatMap(({ scope, description, surface }) => surface.overlay ? [{ + id: `presentation:${scope}:${surface.overlay.overlay_id}`, + title: surface.overlay.title, + description: `${description} · ${surface.overlay.mode === "heatmap" ? "热力分布" : "空间范围"}`, + featureCount: surface.overlay.data.features.length, + color: MAP_STYLE_TOKENS.state.risk as string, + visible: surface.overlay.visible, + disabled: surface.history.selectedId !== null, + onToggle: (visible: boolean) => { + if (surface.overlay?.visible !== visible) surface.toggleOverlay(); + } + }] : []); + if (activeArtifactHasSpatialView && artifactWorkspace.activeArtifact) { + overlayLayers.push({ + id: `artifact:${artifactWorkspace.activeArtifact.id}`, + title: artifactWorkspace.activeArtifact.title, + description: "分析成果 · 空间范围", + featureCount: activeSpatialOverlay!.features.length, + color: MAP_STYLE_TOKENS.state.agent, + visible: impactVisible, + disabled: false, + onToggle: setImpactVisible + }); + } + const layerControlItems = createLayerControlItems( + layerVisibility, availableSourceIds, sourceFeatureCounts, overlayLayers ); const visibleLegendItems = useMemo( () => createVisibleMapLegendItems(layerControlItems), [layerControlItems] ); function handleToggleLayer(layerControlId: string, visible: boolean) { + if (layerControlId === "simulation") { + setImpactVisible(visible); + return; + } + const overlay = overlayLayers.find((item) => item.id === layerControlId); + if (overlay) { + if (!overlay.disabled) overlay.onToggle(visible); + return; + } presentationLayerChangesRef.current.delete(layerControlId); const map = mapRef.current; setLayerVisibility((current) => ({ ...current, [layerControlId]: visible })); - if (layerControlId === "simulation") { - setImpactVisible(visible); - } - if (!map || !mapReady) { return; } @@ -1079,7 +1103,7 @@ function MapWorkbenchContent({ measurement.clear(); drawing.clear(); geocoding.clear(); - handleToggleLayer("simulation", false); + setImpactVisible(false); showMapNotice({ tone: "success", message: "已清空所有高亮和覆盖层。" }); } @@ -1471,10 +1495,10 @@ function MapWorkbenchContent({ {!devPanelOpen ? ( <> -
+
-
+
@@ -1570,7 +1594,7 @@ function MapWorkbenchContent({ {mobileSheet === null && !readingWorkspace?.active && artifactWorkspace.surfaceMode !== "focus" ? (
{ "scada-flow", "water_quality_monitoring_points", "fx_20200423_pipes", - "fx_20200811_dn100_pipes", - "simulation" + "fx_20200811_dn100_pipes" ]) ); expect(items.find((item) => item.id === "fx_20200423_pipes")).toMatchObject({ @@ -104,12 +103,30 @@ describe("workbench supply layer controls", () => { "valves", "reservoirs", "scada-pressure", - "scada-flow", - "simulation" + "scada-flow" ]); }); - it("defines map-matched symbols for model, source GIS, and analysis layers", () => { + it("lists actual presentation overlays with their visibility and removes cleared entries", () => { + const overlays = [ + { id: "presentation:investigation:area", title: "爆管影响范围", description: "Agent 调查 · 空间范围", featureCount: 1, visible: true }, + { id: "presentation:chat:area", title: "投诉点热力", description: "Agent 对话 · 热力分布", featureCount: 8, visible: false } + ]; + const items = createLayerControlItems(INITIAL_LAYER_VISIBILITY, undefined, {}, overlays); + + expect(items.filter((item) => item.group === "analysis")).toMatchObject([ + { id: overlays[0].id, label: "爆管影响范围", visible: true, featureCount: 1 }, + { id: overlays[1].id, label: "投诉点热力", visible: false, featureCount: 8 } + ]); + expect(createVisibleMapLegendItems(items).filter((item) => item.group === "analysis")).toMatchObject([ + { label: "爆管影响范围", layerControlId: overlays[0].id, shape: "area" } + ]); + const cleared = createLayerControlItems(INITIAL_LAYER_VISIBILITY); + expect(cleared.filter((item) => item.group === "analysis")).toEqual([]); + expect(createVisibleMapLegendItems(cleared).filter((item) => item.group === "analysis")).toEqual([]); + }); + + it("defines map-matched symbols for model and source GIS layers", () => { expect(MAP_LEGEND_ITEMS.map((item) => item.id)).toEqual([ "major-pipe", "medium-pipe", @@ -134,8 +151,7 @@ describe("workbench supply layer controls", () => { "source-hydrant", "source-connection", "source-customer-meter", - "source-non-residential-meter", - "impact-area" + "source-non-residential-meter" ]); expect(MAP_LEGEND_ITEMS.find((item) => item.id === "valve")).toMatchObject({ imageSrc: "/map/valve.png" @@ -186,11 +202,6 @@ describe("workbench supply layer controls", () => { ).toMatchObject({ imageSrc: "/map/gis-non-residential-meter.svg" }); - expect(MAP_LEGEND_ITEMS.find((item) => item.id === "impact-area")).toMatchObject({ - groupLabel: "分析覆盖", - layerControlId: "simulation", - shape: "area" - }); }); it("keeps the legend synchronized with visible layer controls", () => { @@ -222,8 +233,7 @@ describe("workbench supply layer controls", () => { const reducedItems = createLayerControlItems({ ...INITIAL_LAYER_VISIBILITY, - valves: false, - simulation: false + valves: false }); const reducedLegendIds = createVisibleMapLegendItems(reducedItems).map((item) => item.id); diff --git a/src/features/workbench/map/map-control-config.ts b/src/features/workbench/map/map-control-config.ts index 3b40860..cbb9225 100644 --- a/src/features/workbench/map/map-control-config.ts +++ b/src/features/workbench/map/map-control-config.ts @@ -15,19 +15,6 @@ import { import { SCADA_ICON_PATHS, SCADA_LAYER_CONTROL_ITEMS, SCADA_LAYER_GROUPS } from "./scada"; import { SUPPLY_ASSET_ICON_PATHS } from "./supply-icons"; -export const WORKBENCH_LAYER_GROUPS: Record = { - simulation: [ - "simulation-impact-fill", - "simulation-impact-outline", - "simulation-burst-halo", - "simulation-burst-point", - "simulation-pipe-label", - "simulation-impact-label", - "simulation-district-label", - "simulation-burst-label" - ] -}; - export const INITIAL_LAYER_VISIBILITY: Record = { ...Object.fromEntries( SUPPLY_LAYER_CATALOG.filter((layer) => layer.id !== "scada").map((layer) => [ @@ -35,8 +22,17 @@ export const INITIAL_LAYER_VISIBILITY: Record = { layer.initialVisible ]) ), - ...Object.fromEntries(SCADA_LAYER_CONTROL_ITEMS.map((item) => [item.id, true])), - simulation: false + ...Object.fromEntries(SCADA_LAYER_CONTROL_ITEMS.map((item) => [item.id, true])) +}; + +export type AnalysisOverlayLayer = { + id: string; + title: string; + description: string; + visible: boolean; + disabled?: boolean; + featureCount: number; + color?: string; }; const SOURCE_CONTROL_LABELS = Object.fromEntries( @@ -421,25 +417,27 @@ export const MAP_LEGEND_ITEMS: MapLegendItem[] = [ imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceNonResidentialMeter, layerControlId: "fx_20200423_non_residential_meters", ...SOURCE_FACILITY_LEGEND_GROUP - }, - { - id: "impact-area", - label: "影响范围", - description: "事故或分析结果", - color: MAP_STYLE_TOKENS.state.incident, - shape: "area", - layerControlId: "simulation", - ...ANALYSIS_LEGEND_GROUP } ]; export function createVisibleMapLegendItems( - layerItems: readonly Pick[] + layerItems: readonly MapLayerControlItem[] ) { const visibleLayerIds = new Set(layerItems.filter((item) => item.visible).map((item) => item.id)); - return MAP_LEGEND_ITEMS.filter((item) => - item.layerControlId ? visibleLayerIds.has(item.layerControlId) : true - ); + return [ + ...MAP_LEGEND_ITEMS.filter((item) => + item.layerControlId ? visibleLayerIds.has(item.layerControlId) : true + ), + ...layerItems.filter((item) => item.group === "analysis" && item.visible).map((item) => ({ + id: item.id, + label: item.label, + description: item.description, + color: item.visual?.color ?? MAP_STYLE_TOKENS.state.risk, + shape: "area" as const, + layerControlId: item.id, + ...ANALYSIS_LEGEND_GROUP + })) + ]; } export const BASE_LAYER_GROUPS = { @@ -502,7 +500,8 @@ export const BASE_LAYER_OPTIONS: BaseLayerOption[] = [ export function createLayerControlItems( layerVisibility: Record, availableSourceIds: readonly WaterNetworkSourceId[] = AVAILABLE_WATER_NETWORK_SOURCE_IDS, - featureCounts: Partial> = {} + featureCounts: Partial> = {}, + analysisOverlays: readonly AnalysisOverlayLayer[] = [] ): MapLayerControlItem[] { const available = new Set(availableSourceIds); const catalogItems = SUPPLY_LAYER_CATALOG.filter( @@ -560,16 +559,18 @@ export function createLayerControlItems( groupDescription: "源数据快照与专项台账", groupDefaultOpen: false })), - { - id: "simulation", - label: "模拟结果", - description: "影响范围与事故标注", - visual: { kind: "area", color: MAP_STYLE_TOKENS.state.incident }, - visible: layerVisibility.simulation, + ...analysisOverlays.map((overlay) => ({ + id: overlay.id, + label: overlay.title, + description: overlay.description, + featureCount: overlay.featureCount, + visual: { kind: "area" as const, color: overlay.color ?? MAP_STYLE_TOKENS.state.risk }, + visible: overlay.visible, + disabled: overlay.disabled, group: "analysis", groupLabel: "分析覆盖", - groupDescription: "随已打开的空间成果显示" - } + groupDescription: "Agent 生成的范围与分析成果" + })) ]; } @@ -589,8 +590,7 @@ export function getWorkbenchLayerIds( .map((layer) => layer.id); } - const layerIds = WORKBENCH_LAYER_GROUPS[groupId] ?? []; - return layerIds.filter((layerId) => map.getLayer(layerId)); + return []; } function isWaterNetworkSourceId(value: string): value is WaterNetworkSourceId { diff --git a/tests/browser/analysis-overlay-layers.e2e.ts b/tests/browser/analysis-overlay-layers.e2e.ts new file mode 100644 index 0000000..286c1ac --- /dev/null +++ b/tests/browser/analysis-overlay-layers.e2e.ts @@ -0,0 +1,54 @@ +import { expect, test } from "@playwright/test"; +import { investigationFixture, mockInvestigationPresentation, selectInvestigation } from "./support/mock-investigation-presentation"; + +test("lists Agent overlays in the layer panel and synchronizes visibility and cleanup", async ({ page }, testInfo) => { + test.setTimeout(60_000); + const task = investigationFixture(1, "覆盖面图层核验"); + const mock = await mockInvestigationPresentation(page, [task]); + await page.emulateMedia({ reducedMotion: "reduce" }); + await selectInvestigation(page, task.title); + await expect.poll(() => page.evaluate(() => Boolean(window.__waterNetworkMap?.getLayer("simulation-backend-analysis-fill")))).toBe(true); + expect(await page.evaluate(() => Boolean(window.__waterNetworkMap?.getSource("simulation-impact-area")))).toBe(false); + const layerTool = page.getByRole("button", { name: /图层:管理地图图层/ }); + await layerTool.click(); + const panel = page.getByRole("region", { name: "图层", exact: true }); + await expect(panel.getByText("模拟结果", { exact: true })).toHaveCount(0); + await expect(panel.getByText("分析覆盖", { exact: true })).toHaveCount(0); + + const command = { action: "set_overlay", overlay_id: "affected-area", title: "爆管影响范围", mode: "geometry", evidence_id: "test:area", data: { + type: "FeatureCollection", features: [{ type: "Feature", properties: {}, geometry: { + type: "Polygon", coordinates: [[[121.48, 31.1], [121.49, 31.1], [121.49, 31.11], [121.48, 31.1]]] + } }] + } }; + await mock.emit(task, command); + const row = panel.getByRole("button", { name: /^爆管影响范围,/ }); + await expect(row).toBeVisible(); + await expect(row).toHaveAttribute("aria-pressed", "true"); + const visibility = () => page.evaluate(() => window.__waterNetworkMap?.getLayoutProperty("investigation-presentation-overlay-fill", "visibility")); + await expect.poll(visibility).toBe("visible"); + await row.click(); + await expect(row).toHaveAttribute("aria-pressed", "false"); + await expect.poll(visibility).toBe("none"); + await layerTool.click(); + const display = page.getByLabel("调查实时展示", { exact: true }); + await display.getByRole("button", { name: "显示覆盖层", exact: true }).click(); + await expect.poll(visibility).toBe("visible"); + await layerTool.click(); + await expect(row).toHaveAttribute("aria-pressed", "true"); + await page.screenshot({ path: testInfo.outputPath("agent-overlay-layer-desktop.png"), animations: "disabled" }); + + await page.setViewportSize({ width: 390, height: 844 }); + await row.scrollIntoViewIfNeeded(); + await expect(row).toBeVisible(); + await row.click(); + await expect.poll(visibility).toBe("none"); + await row.click(); + await expect.poll(visibility).toBe("visible"); + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390); + await page.screenshot({ path: testInfo.outputPath("agent-overlay-layer-mobile.png"), animations: "disabled" }); + + await mock.emit(task, { action: "clear_overlay", overlay_id: "affected-area" }); + await expect(row).toHaveCount(0); + await expect(panel.getByText("分析覆盖", { exact: true })).toHaveCount(0); + expect(mock.mutations).toEqual([]); +}); diff --git a/tests/browser/scada-api-source.e2e.ts b/tests/browser/scada-api-source.e2e.ts index 1f7036f..7eff58e 100644 --- a/tests/browser/scada-api-source.e2e.ts +++ b/tests/browser/scada-api-source.e2e.ts @@ -103,7 +103,7 @@ test("shows data-dependent layer controls only when GeoServer reports features", const layerIds = globalThis.__waterNetworkMap?.getStyle().layers.map((layer) => layer.id) ?? []; return { valveLabel: layerIds.indexOf("workbench-value-label-valves"), - simulationLabel: layerIds.indexOf("simulation-burst-label") + simulationLabel: layerIds.indexOf("simulation-backend-analysis-label") }; })).toMatchObject({ valveLabel: expect.any(Number), @@ -113,7 +113,7 @@ test("shows data-dependent layer controls only when GeoServer reports features", const layerIds = globalThis.__waterNetworkMap?.getStyle().layers.map((layer) => layer.id) ?? []; return [ layerIds.indexOf("workbench-value-label-valves"), - layerIds.indexOf("simulation-burst-label") + layerIds.indexOf("simulation-backend-analysis-label") ]; }); expect(layerOrder[0]).toBeGreaterThanOrEqual(0); diff --git a/tests/browser/workbench-runtime-backend.e2e.ts b/tests/browser/workbench-runtime-backend.e2e.ts index e392db1..c903ee9 100644 --- a/tests/browser/workbench-runtime-backend.e2e.ts +++ b/tests/browser/workbench-runtime-backend.e2e.ts @@ -377,9 +377,8 @@ test("keeps a non-spatial backend result in the analysis workspace without demo await expect.poll(() => page.evaluate(() => ({ backend: window.__waterNetworkMap ?.querySourceFeatures("simulation-backend-analysis").length ?? 0, - demo: window.__waterNetworkMap - ?.querySourceFeatures("simulation-impact-area").length ?? 0 - }))).toEqual({ backend: 0, demo: 0 }); + demoSource: Boolean(window.__waterNetworkMap?.getSource("simulation-impact-area")) + }))).toEqual({ backend: 0, demoSource: false }); }); test("keeps mock conditions while using backend work orders and alerts", async ({ page }) => {