From bd60bfe2d62291f5126c4136b01c22412d241b03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:49:07 +0000 Subject: [PATCH 1/3] Initial plan From cdb2da32b836791b92b4308c80f8d4fc3a55e1d2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 18:08:06 +0000 Subject: [PATCH 2/3] Expand redux test suite coverage for reducers, actions, and store Co-authored-by: jumpinjackie <563860+jumpinjackie@users.noreply.github.com> --- test/actions/map.spec.ts | 116 +++++++- test/reducers/config.spec.ts | 24 +- test/reducers/map-state.spec.ts | 419 ++++++++++++++++++++++++++++- test/reducers/toolbar.spec.ts | 105 ++++++++ test/reducers/viewer.spec.ts | 16 +- test/store/configure-store.spec.ts | 60 +++++ 6 files changed, 735 insertions(+), 5 deletions(-) create mode 100644 test/store/configure-store.spec.ts diff --git a/test/actions/map.spec.ts b/test/actions/map.spec.ts index 0b8edebd..aac8b004 100644 --- a/test/actions/map.spec.ts +++ b/test/actions/map.spec.ts @@ -664,7 +664,10 @@ import { addClientSelectedFeature, clearClientSelection, externalLayersReady, - mapLayerAdded + mapLayerAdded, + setCurrentView, + setMapSwipeMode, + updateMapSwipePosition } from "../../src/actions/map"; import { Client } from "../../src/api/client"; import { ActionType } from "../../src/constants/actions"; @@ -904,6 +907,19 @@ describe("actions/map - action creators", () => { const action = mapLayerAdded("TestMap", layer, SIMPLE_VECTOR_STYLE); expect(action.payload.defaultStyle).toEqual(SIMPLE_VECTOR_STYLE); }); + it("setMapSwipeMode creates correct action", () => { + const action = setMapSwipeMode(true); + expect(action.type).toBe(ActionType.MAP_SET_SWIPE_MODE); + expect(action.payload.active).toBe(true); + + const action2 = setMapSwipeMode(false); + expect(action2.payload.active).toBe(false); + }); + it("updateMapSwipePosition creates correct action", () => { + const action = updateMapSwipePosition(65); + expect(action.type).toBe(ActionType.MAP_UPDATE_SWIPE_POSITION); + expect(action.payload.position).toBe(65); + }); }); describe("actions/map - activateMap thunk", () => { @@ -1105,4 +1121,102 @@ describe("actions/map - activateMap thunk (with session)", () => { expect(dispatched[0].type).toBe(ActionType.MAP_SET_ACTIVE_MAP); expect(dispatched[0].payload).toBe("LazyMap"); }); +}); + +describe("actions/map - setCurrentView thunk", () => { + it("dispatches MAP_SET_VIEW when no previous view exists", () => { + const initialState = createInitialState(); + const map = createMap(); + const mapState = { + [map.Name]: { + ...MAP_STATE_INITIAL_SUB_STATE, + currentView: undefined, + mapguide: { ...MG_INITIAL_SUB_STATE, runtimeMap: map } + } + }; + const state = { + ...initialState, + config: { + ...initialState.config, + activeMapName: map.Name + }, + mapState + }; + const dispatched: any[] = []; + const dispatch = (action: any) => { dispatched.push(action); return action; }; + const getState = () => state as any; + + const view = { x: -87.72, y: 43.74, scale: 70000 }; + const thunk = setCurrentView(view); + thunk(dispatch as any, getState as any); + + expect(dispatched).toHaveLength(1); + expect(dispatched[0].type).toBe(ActionType.MAP_SET_VIEW); + expect(dispatched[0].payload.view.x).toBe(view.x); + expect(dispatched[0].payload.view.y).toBe(view.y); + expect(dispatched[0].payload.view.scale).toBe(view.scale); + }); + + it("does not dispatch MAP_SET_VIEW when the view is the same as the current view", () => { + const initialState = createInitialState(); + const map = createMap(); + const currentView = { x: -87.72, y: 43.74, scale: 70000 }; + const mapState = { + [map.Name]: { + ...MAP_STATE_INITIAL_SUB_STATE, + currentView, + mapguide: { ...MG_INITIAL_SUB_STATE, runtimeMap: map } + } + }; + const state = { + ...initialState, + config: { + ...initialState.config, + activeMapName: map.Name + }, + mapState + }; + const dispatched: any[] = []; + const dispatch = (action: any) => { dispatched.push(action); return action; }; + const getState = () => state as any; + + const thunk = setCurrentView({ ...currentView }); + thunk(dispatch as any, getState as any); + + expect(dispatched).toHaveLength(0); + }); + + it("dispatches MAP_SET_VIEW when the view differs from the current view", () => { + const initialState = createInitialState(); + const map = createMap(); + const currentView = { x: -87.72, y: 43.74, scale: 70000 }; + const mapState = { + [map.Name]: { + ...MAP_STATE_INITIAL_SUB_STATE, + currentView, + mapguide: { ...MG_INITIAL_SUB_STATE, runtimeMap: map } + } + }; + const state = { + ...initialState, + config: { + ...initialState.config, + activeMapName: map.Name + }, + mapState + }; + const dispatched: any[] = []; + const dispatch = (action: any) => { dispatched.push(action); return action; }; + const getState = () => state as any; + + const newView = { x: -87.50, y: 43.50, scale: 50000 }; + const thunk = setCurrentView(newView); + thunk(dispatch as any, getState as any); + + expect(dispatched).toHaveLength(1); + expect(dispatched[0].type).toBe(ActionType.MAP_SET_VIEW); + expect(dispatched[0].payload.view.x).toBe(newView.x); + expect(dispatched[0].payload.view.y).toBe(newView.y); + expect(dispatched[0].payload.view.scale).toBe(newView.scale); + }); }); \ No newline at end of file diff --git a/test/reducers/config.spec.ts b/test/reducers/config.spec.ts index 3df91770..aafe97aa 100644 --- a/test/reducers/config.spec.ts +++ b/test/reducers/config.spec.ts @@ -5,7 +5,7 @@ import { import { createMap, createInitAction, createInitialState } from "../../test-data"; import { configReducer } from "../../src/reducers/config"; import { ActionType } from "../../src/constants/actions"; -import { setActiveMap, setManualFeatureTooltipsEnabled, setViewRotation, setViewRotationEnabled, setViewSizeUnits } from "../../src/actions/map"; +import { enableSelectDragPan, setActiveMap, setManualFeatureTooltipsEnabled, setViewRotation, setViewRotationEnabled, setViewSizeUnits } from "../../src/actions/map"; import { ViewerAction } from "../../src/actions/defs"; describe("reducers/config", () => { @@ -204,4 +204,26 @@ describe("reducers/config", () => { expect(state.swipePosition).toBe(75); }); }); + describe(ActionType.SET_LOCALE, () => { + it("updates locale", () => { + const initialState = createInitialState(); + const action: ViewerAction = { type: ActionType.SET_LOCALE, payload: "fr" }; + const state = configReducer(initialState.config, action); + expect(state.locale).toBe("fr"); + }); + }); + describe(ActionType.MAP_ENABLE_SELECT_DRAGPAN, () => { + it("updates selectCanDragPan when enabled", () => { + const initialState = createInitialState(); + const action = enableSelectDragPan(true); + const state = configReducer(initialState.config, action); + expect(state.selectCanDragPan).toBe(true); + }); + it("updates selectCanDragPan when disabled", () => { + const initialState = createInitialState(); + const action = enableSelectDragPan(false); + const state = configReducer(initialState.config, action); + expect(state.selectCanDragPan).toBe(false); + }); + }); }); \ No newline at end of file diff --git a/test/reducers/map-state.spec.ts b/test/reducers/map-state.spec.ts index b069159d..9fb0315b 100644 --- a/test/reducers/map-state.spec.ts +++ b/test/reducers/map-state.spec.ts @@ -1,11 +1,36 @@ import { describe, it, expect, vi } from "vitest"; import { IMapSetViewAction } from "../../src/actions/defs"; import { setGroupExpanded, setGroupVisibility, setLayerSelectable, setLayerVisibility } from "../../src/actions/legend"; -import { addClientSelectedFeature, clearClientSelection, nextView, previousView, setBaseLayer, setCurrentView, setScale, setSelection } from "../../src/actions/map"; +import { + addClientSelectedFeature, + addMapLayerBusyWorker, + clearClientSelection, + externalLayersReady, + mapLayerAdded, + nextView, + previousView, + removeMapLayer, + removeMapLayerBusyWorker, + setBaseLayer, + setCurrentView, + setHeatmapLayerBlur, + setHeatmapLayerRadius, + setLayerTransparency, + setMapLayerIndex, + setMapLayerOpacity, + setMapLayerVectorStyle, + setMapLayerVisibility, + setScale, + setSelection, + showSelectedFeature +} from "../../src/actions/map"; +import type { ILayerInfo } from "../../src/api/common"; import { IMapView } from "../../src/api/common"; import { RuntimeMap } from "../../src/api/contracts/runtime-map"; import { ActionType } from "../../src/constants/actions"; -import { mapStateReducer } from "../../src/reducers/map-state"; +import { VectorStyleSource } from "../../src/api/ol-style-contracts"; +import type { IVectorLayerStyle } from "../../src/api/ol-style-contracts"; +import { mapStateReducer, MG_INITIAL_SUB_STATE, MAP_STATE_INITIAL_SUB_STATE } from "../../src/reducers/map-state"; import { createMap, createInitAction, createInitialState, createQueryMapFeaturesResponse } from "../../test-data"; describe("reducers/config", () => { @@ -408,4 +433,394 @@ describe("reducers/config", () => { expect(ms.currentView?.y).toBe(view2.y); expect(ms.currentView?.scale).toBe(view2.scale); }); + + describe(ActionType.MAP_REFRESH, () => { + it("updates the runtime map", () => { + const initialState = createInitialState(); + const map: RuntimeMap = createMap(); + const view: IMapView = { x: -87.72, y: 43.74, scale: 70000 }; + const initAction = createInitAction(map, view, "en"); + const state = mapStateReducer(initialState.mapState, initAction); + + const updatedMap = { ...map, SiteVersion: "4.0.0" }; + const refreshAction: any = { + type: ActionType.MAP_REFRESH, + payload: { mapName: map.Name, map: updatedMap } + }; + const state2 = mapStateReducer(state, refreshAction); + const ms = state2[map.Name]; + expect(ms).not.toBeUndefined(); + expect(ms.mapguide).not.toBeUndefined(); + expect(ms.mapguide?.runtimeMap).toBe(updatedMap); + }); + }); + + describe(ActionType.MAP_SET_LAYER_TRANSPARENCY, () => { + it("updates layer transparency in mapguide sub-state", () => { + const initialState = createInitialState(); + const map: RuntimeMap = createMap(); + const view: IMapView = { x: -87.72, y: 43.74, scale: 70000 }; + const initAction = createInitAction(map, view, "en"); + const state = mapStateReducer(initialState.mapState, initAction); + + const action = setLayerTransparency(map.Name, "Roads", 0.5); + const state2 = mapStateReducer(state, action); + const ms = state2[map.Name]; + expect(ms).not.toBeUndefined(); + expect(ms.mapguide).not.toBeUndefined(); + expect(ms.mapguide?.layerTransparency["Roads"]).toBe(0.5); + }); + }); + + describe(ActionType.MAP_SHOW_SELECTED_FEATURE, () => { + it("sets the active selected feature in mapguide sub-state", () => { + const initialState = createInitialState(); + const map: RuntimeMap = createMap(); + const view: IMapView = { x: -87.72, y: 43.74, scale: 70000 }; + const initAction = createInitAction(map, view, "en"); + const state = mapStateReducer(initialState.mapState, initAction); + + const action = showSelectedFeature(map.Name, "layer-01", "key-42"); + const state2 = mapStateReducer(state, action); + const ms = state2[map.Name]; + expect(ms).not.toBeUndefined(); + expect(ms.mapguide).not.toBeUndefined(); + expect(ms.mapguide?.activeSelectedFeature).not.toBeUndefined(); + expect(ms.mapguide?.activeSelectedFeature?.layerId).toBe("layer-01"); + expect(ms.mapguide?.activeSelectedFeature?.selectionKey).toBe("key-42"); + }); + }); + + describe(ActionType.EXTERNAL_LAYERS_READY, () => { + it("sets the layers array on the map sub-state", () => { + const initialState = createInitialState(); + const map: RuntimeMap = createMap(); + const view: IMapView = { x: -87.72, y: 43.74, scale: 70000 }; + const initAction = createInitAction(map, view, "en"); + const state = mapStateReducer(initialState.mapState, initAction); + + const action = externalLayersReady(map.Name); + const state2 = mapStateReducer(state, action); + const ms = state2[map.Name]; + expect(ms).not.toBeUndefined(); + expect(ms.layers).not.toBeUndefined(); + }); + }); + + describe(ActionType.LAYER_ADDED, () => { + it("prepends a new layer to the layers array", () => { + const initialState = createInitialState(); + const map: RuntimeMap = createMap(); + const view: IMapView = { x: -87.72, y: 43.74, scale: 70000 }; + const initAction = createInitAction(map, view, "en"); + const state = mapStateReducer(initialState.mapState, initAction); + + const layer: ILayerInfo = { + name: "MyLayer", + displayName: "My Layer", + type: "WMS", + isExternal: true, + visible: true, + selectable: false, + opacity: 1, + busyWorkerCount: 0 + }; + const action = mapLayerAdded(map.Name, layer); + const state2 = mapStateReducer(state, action); + const ms = state2[map.Name]; + expect(ms).not.toBeUndefined(); + expect(ms.layers).not.toBeUndefined(); + expect(ms.layers).toHaveLength(1); + expect(ms.layers![0].name).toBe("MyLayer"); + }); + + it("prepends a layer with a default style when provided", () => { + const initialState = createInitialState(); + const map: RuntimeMap = createMap(); + const view: IMapView = { x: -87.72, y: 43.74, scale: 70000 }; + const initAction = createInitAction(map, view, "en"); + const state = mapStateReducer(initialState.mapState, initAction); + + const layer: ILayerInfo = { + name: "StyledLayer", + displayName: "Styled Layer", + type: "Vector", + isExternal: true, + visible: true, + selectable: true, + opacity: 1, + busyWorkerCount: 0 + }; + const defaultStyle: IVectorLayerStyle = { + default: { + point: { type: "Circle", radius: 5, fill: { color: "#ff0000", alpha: 255 }, stroke: { color: "#000000", width: 1, alpha: 255 } } + } + }; + const action = mapLayerAdded(map.Name, layer, defaultStyle); + const state2 = mapStateReducer(state, action); + const ms = state2[map.Name]; + expect(ms).not.toBeUndefined(); + expect(ms.layers).not.toBeUndefined(); + expect(ms.layers![0].vectorStyle).toBe(defaultStyle); + }); + }); + + describe(ActionType.REMOVE_LAYER, () => { + it("removes a layer from the layers array by name", () => { + const initialState = createInitialState(); + const map: RuntimeMap = createMap(); + const view: IMapView = { x: -87.72, y: 43.74, scale: 70000 }; + const initAction = createInitAction(map, view, "en"); + const state = mapStateReducer(initialState.mapState, initAction); + + const layer: ILayerInfo = { + name: "LayerToRemove", + displayName: "Layer To Remove", + type: "WMS", + isExternal: true, + visible: true, + selectable: false, + opacity: 1, + busyWorkerCount: 0 + }; + const addedState = mapStateReducer(state, mapLayerAdded(map.Name, layer)); + expect(addedState[map.Name].layers).toHaveLength(1); + + const action = removeMapLayer(map.Name, "LayerToRemove"); + const state2 = mapStateReducer(addedState, action); + const ms = state2[map.Name]; + expect(ms).not.toBeUndefined(); + expect(ms.layers).toHaveLength(0); + }); + }); + + describe(ActionType.SET_LAYER_VISIBILITY, () => { + it("sets layer visibility by layer name", () => { + const initialState = createInitialState(); + const map: RuntimeMap = createMap(); + const view: IMapView = { x: -87.72, y: 43.74, scale: 70000 }; + const initAction = createInitAction(map, view, "en"); + const state = mapStateReducer(initialState.mapState, initAction); + + const layer: ILayerInfo = { + name: "VisLayer", + displayName: "Visible Layer", + type: "WMS", + isExternal: true, + visible: true, + selectable: false, + opacity: 1, + busyWorkerCount: 0 + }; + const addedState = mapStateReducer(state, mapLayerAdded(map.Name, layer)); + + const action = setMapLayerVisibility(map.Name, "VisLayer", false); + const state2 = mapStateReducer(addedState, action); + const ms = state2[map.Name]; + expect(ms.layers![0].visible).toBe(false); + + const state3 = mapStateReducer(state2, setMapLayerVisibility(map.Name, "VisLayer", true)); + expect(state3[map.Name].layers![0].visible).toBe(true); + }); + }); + + describe(ActionType.SET_LAYER_OPACITY, () => { + it("sets layer opacity by layer name", () => { + const initialState = createInitialState(); + const map: RuntimeMap = createMap(); + const view: IMapView = { x: -87.72, y: 43.74, scale: 70000 }; + const initAction = createInitAction(map, view, "en"); + const state = mapStateReducer(initialState.mapState, initAction); + + const layer: ILayerInfo = { + name: "OpacityLayer", + displayName: "Opacity Layer", + type: "WMS", + isExternal: true, + visible: true, + selectable: false, + opacity: 1, + busyWorkerCount: 0 + }; + const addedState = mapStateReducer(state, mapLayerAdded(map.Name, layer)); + + const action = setMapLayerOpacity(map.Name, "OpacityLayer", 0.5); + const state2 = mapStateReducer(addedState, action); + expect(state2[map.Name].layers![0].opacity).toBe(0.5); + }); + }); + + describe(ActionType.SET_HEATMAP_LAYER_BLUR, () => { + it("sets heatmap layer blur by layer name", () => { + const initialState = createInitialState(); + const map: RuntimeMap = createMap(); + const view: IMapView = { x: -87.72, y: 43.74, scale: 70000 }; + const initAction = createInitAction(map, view, "en"); + const state = mapStateReducer(initialState.mapState, initAction); + + const layer: ILayerInfo = { + name: "HeatmapLayer", + displayName: "Heatmap Layer", + type: "Heatmap", + isExternal: true, + visible: true, + selectable: false, + opacity: 1, + busyWorkerCount: 0 + }; + const addedState = mapStateReducer(state, mapLayerAdded(map.Name, layer)); + + const action = setHeatmapLayerBlur(map.Name, "HeatmapLayer", 20); + const state2 = mapStateReducer(addedState, action); + expect(state2[map.Name].layers![0].heatmap?.blur).toBe(20); + }); + }); + + describe(ActionType.SET_HEATMAP_LAYER_RADIUS, () => { + it("sets heatmap layer radius by layer name", () => { + const initialState = createInitialState(); + const map: RuntimeMap = createMap(); + const view: IMapView = { x: -87.72, y: 43.74, scale: 70000 }; + const initAction = createInitAction(map, view, "en"); + const state = mapStateReducer(initialState.mapState, initAction); + + const layer: ILayerInfo = { + name: "HeatmapLayer", + displayName: "Heatmap Layer", + type: "Heatmap", + isExternal: true, + visible: true, + selectable: false, + opacity: 1, + busyWorkerCount: 0 + }; + const addedState = mapStateReducer(state, mapLayerAdded(map.Name, layer)); + + const action = setHeatmapLayerRadius(map.Name, "HeatmapLayer", 8); + const state2 = mapStateReducer(addedState, action); + expect(state2[map.Name].layers![0].heatmap?.radius).toBe(8); + }); + }); + + describe(ActionType.SET_LAYER_INDEX, () => { + it("moves a layer from one index to another", () => { + const initialState = createInitialState(); + const map: RuntimeMap = createMap(); + const view: IMapView = { x: -87.72, y: 43.74, scale: 70000 }; + const initAction = createInitAction(map, view, "en"); + const state = mapStateReducer(initialState.mapState, initAction); + + const makeLayer = (name: string): ILayerInfo => ({ + name, + displayName: name, + type: "WMS", + isExternal: true, + visible: true, + selectable: false, + opacity: 1, + busyWorkerCount: 0 + }); + + // Add layers in order: C, B, A (prepend, so state becomes A, B, C after three adds) + let s = mapStateReducer(state, mapLayerAdded(map.Name, makeLayer("LayerC"))); + s = mapStateReducer(s, mapLayerAdded(map.Name, makeLayer("LayerB"))); + s = mapStateReducer(s, mapLayerAdded(map.Name, makeLayer("LayerA"))); + // Layers are now [A, B, C] (index 0, 1, 2) + expect(s[map.Name].layers![0].name).toBe("LayerA"); + expect(s[map.Name].layers![1].name).toBe("LayerB"); + expect(s[map.Name].layers![2].name).toBe("LayerC"); + + // Move LayerA (index 0) to index 2 + const action = setMapLayerIndex(map.Name, "LayerA", 2); + const s2 = mapStateReducer(s, action); + expect(s2[map.Name].layers![0].name).toBe("LayerB"); + expect(s2[map.Name].layers![1].name).toBe("LayerC"); + expect(s2[map.Name].layers![2].name).toBe("LayerA"); + }); + }); + + describe(ActionType.SET_LAYER_VECTOR_STYLE, () => { + it("sets a base vector style on a layer", () => { + const initialState = createInitialState(); + const map: RuntimeMap = createMap(); + const view: IMapView = { x: -87.72, y: 43.74, scale: 70000 }; + const initAction = createInitAction(map, view, "en"); + const state = mapStateReducer(initialState.mapState, initAction); + + const layer: ILayerInfo = { + name: "VectorLayer", + displayName: "Vector Layer", + type: "Vector", + isExternal: true, + visible: true, + selectable: true, + opacity: 1, + busyWorkerCount: 0 + }; + const addedState = mapStateReducer(state, mapLayerAdded(map.Name, layer)); + + const style: IVectorLayerStyle = { + default: { + point: { type: "Circle", radius: 6, fill: { color: "#00ff00", alpha: 200 }, stroke: { color: "#000000", width: 2, alpha: 255 } } + } + }; + const action = setMapLayerVectorStyle(map.Name, "VectorLayer", style, VectorStyleSource.Base); + const state2 = mapStateReducer(addedState, action); + expect(state2[map.Name].layers![0].vectorStyle).toBe(style); + }); + }); + + describe(ActionType.ADD_LAYER_BUSY_WORKER, () => { + it("increments busyWorkerCount for a layer", () => { + const initialState = createInitialState(); + const map: RuntimeMap = createMap(); + const view: IMapView = { x: -87.72, y: 43.74, scale: 70000 }; + const initAction = createInitAction(map, view, "en"); + const state = mapStateReducer(initialState.mapState, initAction); + + const layer: ILayerInfo = { + name: "BusyLayer", + displayName: "Busy Layer", + type: "WMS", + isExternal: true, + visible: true, + selectable: false, + opacity: 1, + busyWorkerCount: 0 + }; + const addedState = mapStateReducer(state, mapLayerAdded(map.Name, layer)); + expect(addedState[map.Name].layers![0].busyWorkerCount).toBe(0); + + const action = addMapLayerBusyWorker(map.Name, "BusyLayer"); + const state2 = mapStateReducer(addedState, action); + expect(state2[map.Name].layers![0].busyWorkerCount).toBe(1); + }); + }); + + describe(ActionType.REMOVE_LAYER_BUSY_WORKER, () => { + it("decrements busyWorkerCount for a layer", () => { + const initialState = createInitialState(); + const map: RuntimeMap = createMap(); + const view: IMapView = { x: -87.72, y: 43.74, scale: 70000 }; + const initAction = createInitAction(map, view, "en"); + const state = mapStateReducer(initialState.mapState, initAction); + + const layer: ILayerInfo = { + name: "BusyLayer", + displayName: "Busy Layer", + type: "WMS", + isExternal: true, + visible: true, + selectable: false, + opacity: 1, + busyWorkerCount: 2 + }; + const addedState = mapStateReducer(state, mapLayerAdded(map.Name, layer)); + expect(addedState[map.Name].layers![0].busyWorkerCount).toBe(2); + + const action = removeMapLayerBusyWorker(map.Name, "BusyLayer"); + const state2 = mapStateReducer(addedState, action); + expect(state2[map.Name].layers![0].busyWorkerCount).toBe(1); + }); + }); }); \ No newline at end of file diff --git a/test/reducers/toolbar.spec.ts b/test/reducers/toolbar.spec.ts index 185769f1..ba730e6f 100644 --- a/test/reducers/toolbar.spec.ts +++ b/test/reducers/toolbar.spec.ts @@ -4,6 +4,7 @@ import { ICommandSpec, IFlyoutSpec } from "../../src/api/registry/command-spec"; import { ActionType } from "../../src/constants/actions"; import { toolbarReducer } from "../../src/reducers/toolbar"; import { createInitAction, createInitialState, createMap } from "../../test-data"; +import { WEBLAYOUT_CONTEXTMENU, WEBLAYOUT_TASKMENU } from "../../src/constants"; describe("reducers/mouse", () => { describe(ActionType.INIT_APP, () => { @@ -811,4 +812,108 @@ describe("reducers/mouse", () => { expect((newState.flyouts[contextMenuId] as any).open).toBe(true); }); }); + + describe(ActionType.COMPONENT_CLOSE, () => { + it("returns state unchanged when context menu flyout is not registered", () => { + const initialState = createInitialState(); + const action: any = { + type: ActionType.COMPONENT_CLOSE, + payload: { flyoutId: "someComponent" } + }; + const newState = toolbarReducer(initialState.toolbar, action); + expect(newState).toBe(initialState.toolbar); + }); + it("closes a component flyout when context menu is registered", () => { + const initialState = createInitialState(); + const componentFlyoutId = "myComponentFlyout"; + const stateWithFlyouts = { + ...initialState.toolbar, + flyouts: { + [WEBLAYOUT_CONTEXTMENU]: { open: false, metrics: null }, + [componentFlyoutId]: { open: true, metrics: { posX: 10, posY: 20, width: 0, height: 0 }, componentName: "MyComponent", componentProps: {} } + } + }; + const action: any = { + type: ActionType.COMPONENT_CLOSE, + payload: { flyoutId: componentFlyoutId } + }; + const newState = toolbarReducer(stateWithFlyouts as any, action); + expect((newState.flyouts[componentFlyoutId] as any).open).toBe(false); + expect((newState.flyouts[componentFlyoutId] as any).componentName).toBeNull(); + expect((newState.flyouts[componentFlyoutId] as any).componentProps).toBeNull(); + }); + }); + describe(ActionType.FUSION_SET_ELEMENT_STATE, () => { + it("closes the task menu flyout when taskPaneVisible is false and task menu is registered", () => { + const initialState = createInitialState(); + const stateWithTaskMenu = { + ...initialState.toolbar, + flyouts: { + [WEBLAYOUT_TASKMENU]: { open: true, metrics: { posX: 0, posY: 0, width: 0, height: 0 } } + } + }; + const action: any = { + type: ActionType.FUSION_SET_ELEMENT_STATE, + payload: { legendVisible: true, taskPaneVisible: false, selectionPanelVisible: true } + }; + const newState = toolbarReducer(stateWithTaskMenu as any, action); + expect((newState.flyouts[WEBLAYOUT_TASKMENU] as any).open).toBe(false); + }); + it("returns state unchanged when taskPaneVisible is true", () => { + const initialState = createInitialState(); + const stateWithTaskMenu = { + ...initialState.toolbar, + flyouts: { + [WEBLAYOUT_TASKMENU]: { open: true, metrics: { posX: 0, posY: 0, width: 0, height: 0 } } + } + }; + const action: any = { + type: ActionType.FUSION_SET_ELEMENT_STATE, + payload: { legendVisible: true, taskPaneVisible: true, selectionPanelVisible: true } + }; + const newState = toolbarReducer(stateWithTaskMenu as any, action); + expect(newState).toBe(stateWithTaskMenu); + }); + it("returns state unchanged when task menu is not registered", () => { + const initialState = createInitialState(); + const action: any = { + type: ActionType.FUSION_SET_ELEMENT_STATE, + payload: { legendVisible: true, taskPaneVisible: false, selectionPanelVisible: true } + }; + const newState = toolbarReducer(initialState.toolbar, action); + expect(newState).toBe(initialState.toolbar); + }); + }); + describe(ActionType.FUSION_SET_TASK_PANE_VISIBILITY, () => { + it("closes the task menu flyout when visibility is false", () => { + const initialState = createInitialState(); + const stateWithTaskMenu = { + ...initialState.toolbar, + flyouts: { + [WEBLAYOUT_TASKMENU]: { open: true, metrics: { posX: 0, posY: 0, width: 0, height: 0 } } + } + }; + const action: any = { + type: ActionType.FUSION_SET_TASK_PANE_VISIBILITY, + payload: false + }; + const newState = toolbarReducer(stateWithTaskMenu as any, action); + expect((newState.flyouts[WEBLAYOUT_TASKMENU] as any).open).toBe(false); + }); + it("returns state unchanged when visibility is true", () => { + const initialState = createInitialState(); + const stateWithTaskMenu = { + ...initialState.toolbar, + flyouts: { + [WEBLAYOUT_TASKMENU]: { open: true, metrics: { posX: 0, posY: 0, width: 0, height: 0 } } + } + }; + const action: any = { + type: ActionType.FUSION_SET_TASK_PANE_VISIBILITY, + payload: true + }; + const newState = toolbarReducer(stateWithTaskMenu as any, action); + expect(newState).toBe(stateWithTaskMenu); + }); + }); }); \ No newline at end of file diff --git a/test/reducers/viewer.spec.ts b/test/reducers/viewer.spec.ts index 2b842eef..676176a6 100644 --- a/test/reducers/viewer.spec.ts +++ b/test/reducers/viewer.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { mapResized, setActiveTool, setBusyCount, setFeatureTooltipsEnabled } from "../../src/actions/map"; +import { enableSelectDragPan, mapResized, setActiveTool, setBusyCount, setFeatureTooltipsEnabled } from "../../src/actions/map"; import { ActiveMapTool, IMapView } from "../../src/api/common"; import { ActionType } from "../../src/constants/actions"; import { viewerReducer } from "../../src/reducers/viewer"; @@ -58,4 +58,18 @@ describe("reducers/viewer", () => { expect(state.size![1]).toBe(action.payload.height); }); }); + describe(ActionType.MAP_ENABLE_SELECT_DRAGPAN, () => { + it("updates selectCanDragPan when enabled", () => { + const initialState = createInitialState(); + const action = enableSelectDragPan(true); + const state = viewerReducer(initialState.viewer, action); + expect(state.selectCanDragPan).toBe(true); + }); + it("updates selectCanDragPan when disabled", () => { + const initialState = createInitialState(); + const action = enableSelectDragPan(false); + const state = viewerReducer(initialState.viewer, action); + expect(state.selectCanDragPan).toBe(false); + }); + }); }); \ No newline at end of file diff --git a/test/store/configure-store.spec.ts b/test/store/configure-store.spec.ts new file mode 100644 index 00000000..ca66631c --- /dev/null +++ b/test/store/configure-store.spec.ts @@ -0,0 +1,60 @@ +import { describe, it, expect } from "vitest"; +import { configureStore } from "../../src/store/configure-store"; +import { ActionType } from "../../src/constants/actions"; +import type { ViewerAction } from "../../src/actions/defs"; + +describe("store/configure-store", () => { + it("creates a store with default root reducers", () => { + const store = configureStore({}); + const state = store.getState(); + + expect(state).not.toBeNull(); + expect(state.initError).not.toBeUndefined(); + expect(state.config).not.toBeUndefined(); + expect(state.template).not.toBeUndefined(); + expect(state.mapState).not.toBeUndefined(); + expect(state.viewer).not.toBeUndefined(); + expect(state.toolbar).not.toBeUndefined(); + expect(state.taskpane).not.toBeUndefined(); + expect(state.modal).not.toBeUndefined(); + expect(state.mouse).not.toBeUndefined(); + expect(state.lastaction).not.toBeUndefined(); + }); + + it("creates a store with extra reducers when provided", () => { + const customReducer = (state = { value: 42 }, _action: ViewerAction) => state; + const store = configureStore({}, { custom: customReducer }); + const state = store.getState() as ReturnType & { custom: { value: number } }; + + expect(state.custom).not.toBeUndefined(); + expect(state.custom.value).toBe(42); + }); + + it("dispatches actions that are processed by the root reducers", () => { + const store = configureStore({}); + const initialBusyCount = store.getState().viewer.busyCount; + expect(initialBusyCount).toBe(0); + + const action: ViewerAction = { + type: ActionType.MAP_SET_BUSY_COUNT, + payload: 3 + }; + store.dispatch(action); + + expect(store.getState().viewer.busyCount).toBe(3); + }); + + it("uses provided initial state", () => { + const initialState = { + viewer: { + busyCount: 5, + size: undefined, + tool: 0, + featureTooltipsEnabled: false + } + }; + const store = configureStore(initialState); + expect(store.getState().viewer.busyCount).toBe(5); + expect(store.getState().viewer.featureTooltipsEnabled).toBe(false); + }); +}); From a385d4d9459bca6615378e97f3150556bab548a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 18:21:11 +0000 Subject: [PATCH 3/3] Fix TypeScript build errors in redux test files Co-authored-by: jumpinjackie <563860+jumpinjackie@users.noreply.github.com> --- test/reducers/map-state.spec.ts | 4 ++-- test/reducers/viewer.spec.ts | 16 +--------------- test/store/configure-store.spec.ts | 15 ++++++++++----- 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/test/reducers/map-state.spec.ts b/test/reducers/map-state.spec.ts index 9fb0315b..46d95989 100644 --- a/test/reducers/map-state.spec.ts +++ b/test/reducers/map-state.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from "vitest"; -import { IMapSetViewAction } from "../../src/actions/defs"; +import { IMapSetViewAction, IExternalLayersReadyAction } from "../../src/actions/defs"; import { setGroupExpanded, setGroupVisibility, setLayerSelectable, setLayerVisibility } from "../../src/actions/legend"; import { addClientSelectedFeature, @@ -499,7 +499,7 @@ describe("reducers/config", () => { const initAction = createInitAction(map, view, "en"); const state = mapStateReducer(initialState.mapState, initAction); - const action = externalLayersReady(map.Name); + const action = externalLayersReady(map.Name) as IExternalLayersReadyAction; const state2 = mapStateReducer(state, action); const ms = state2[map.Name]; expect(ms).not.toBeUndefined(); diff --git a/test/reducers/viewer.spec.ts b/test/reducers/viewer.spec.ts index 676176a6..2b842eef 100644 --- a/test/reducers/viewer.spec.ts +++ b/test/reducers/viewer.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { enableSelectDragPan, mapResized, setActiveTool, setBusyCount, setFeatureTooltipsEnabled } from "../../src/actions/map"; +import { mapResized, setActiveTool, setBusyCount, setFeatureTooltipsEnabled } from "../../src/actions/map"; import { ActiveMapTool, IMapView } from "../../src/api/common"; import { ActionType } from "../../src/constants/actions"; import { viewerReducer } from "../../src/reducers/viewer"; @@ -58,18 +58,4 @@ describe("reducers/viewer", () => { expect(state.size![1]).toBe(action.payload.height); }); }); - describe(ActionType.MAP_ENABLE_SELECT_DRAGPAN, () => { - it("updates selectCanDragPan when enabled", () => { - const initialState = createInitialState(); - const action = enableSelectDragPan(true); - const state = viewerReducer(initialState.viewer, action); - expect(state.selectCanDragPan).toBe(true); - }); - it("updates selectCanDragPan when disabled", () => { - const initialState = createInitialState(); - const action = enableSelectDragPan(false); - const state = viewerReducer(initialState.viewer, action); - expect(state.selectCanDragPan).toBe(false); - }); - }); }); \ No newline at end of file diff --git a/test/store/configure-store.spec.ts b/test/store/configure-store.spec.ts index ca66631c..be73d87d 100644 --- a/test/store/configure-store.spec.ts +++ b/test/store/configure-store.spec.ts @@ -1,11 +1,16 @@ import { describe, it, expect } from "vitest"; +import type { Store } from "redux"; import { configureStore } from "../../src/store/configure-store"; import { ActionType } from "../../src/constants/actions"; import type { ViewerAction } from "../../src/actions/defs"; +import type { IApplicationState } from "../../src/api/common"; + +// configureStore uses redux compose(), which loses TypeScript generics. Declare a typed alias. +type AppStore = Store, ViewerAction>; describe("store/configure-store", () => { it("creates a store with default root reducers", () => { - const store = configureStore({}); + const store = configureStore({}) as unknown as AppStore; const state = store.getState(); expect(state).not.toBeNull(); @@ -23,15 +28,15 @@ describe("store/configure-store", () => { it("creates a store with extra reducers when provided", () => { const customReducer = (state = { value: 42 }, _action: ViewerAction) => state; - const store = configureStore({}, { custom: customReducer }); - const state = store.getState() as ReturnType & { custom: { value: number } }; + const store = configureStore({}, { custom: customReducer }) as unknown as AppStore; + const state = store.getState() as Readonly & { custom: { value: number } }; expect(state.custom).not.toBeUndefined(); expect(state.custom.value).toBe(42); }); it("dispatches actions that are processed by the root reducers", () => { - const store = configureStore({}); + const store = configureStore({}) as unknown as AppStore; const initialBusyCount = store.getState().viewer.busyCount; expect(initialBusyCount).toBe(0); @@ -53,7 +58,7 @@ describe("store/configure-store", () => { featureTooltipsEnabled: false } }; - const store = configureStore(initialState); + const store = configureStore(initialState) as unknown as AppStore; expect(store.getState().viewer.busyCount).toBe(5); expect(store.getState().viewer.featureTooltipsEnabled).toBe(false); });