diff --git a/.eslintignore b/.eslintignore index c32059fff..bb2ad5bf6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ node_modules/ build/ +coverage/ webpack.config.js src/locales cypress/fixtures/* diff --git a/.gitignore b/.gitignore index b76d3f46d..dc3f7379f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules build +coverage cypress/videos/ cypress/screenshots/ cypress/downloads/ diff --git a/.prettierignore b/.prettierignore index 5424181a0..957b836b9 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,6 +2,7 @@ node_modules .d2 src/locales build +coverage cypress/fixtures/* cypress.env.json docs/maps.md diff --git a/jest.config.js b/jest.config.js index a245c3f96..29d35b2a6 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,6 @@ module.exports = { setupFilesAfterEnv: ['/config/testSetup.js'], - collectCoverageFrom: ['src/**/*.js'], + collectCoverageFrom: ['src/**/*.{js,jsx}'], testPathIgnorePatterns: ['/node_modules/', '/cypress/'], transformIgnorePatterns: [ '/node_modules/(?!d3-(array|axis|color|format|interpolate|scale|selection|time)|internmap|array-move)', diff --git a/package.json b/package.json index bcb4b21bd..7046ec366 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "build": "d2-app-scripts build", "start": "d2-app-scripts start", "test": "d2-app-scripts test", + "test:coverage": "d2-app-scripts test --coverage", "deploy": "d2-app-scripts deploy", "lint": "d2-style check", "format": "d2-style apply", diff --git a/src/actions/__tests__/aggregations.spec.js b/src/actions/__tests__/aggregations.spec.js new file mode 100644 index 000000000..a30aeb431 --- /dev/null +++ b/src/actions/__tests__/aggregations.spec.js @@ -0,0 +1,13 @@ +import * as types from '../../constants/actionTypes.js' +import { setAggregations } from '../aggregations.js' + +describe('setAggregations', () => { + it('creates an AGGREGATIONS_SET action', () => { + const payload = { count: 'sum' } + + expect(setAggregations(payload)).toEqual({ + type: types.AGGREGATIONS_SET, + payload, + }) + }) +}) diff --git a/src/actions/__tests__/analyticalObject.spec.js b/src/actions/__tests__/analyticalObject.spec.js new file mode 100644 index 000000000..174b9587d --- /dev/null +++ b/src/actions/__tests__/analyticalObject.spec.js @@ -0,0 +1,24 @@ +import * as types from '../../constants/actionTypes.js' +import { + setAnalyticalObject, + clearAnalyticalObject, +} from '../analyticalObject.js' + +describe('setAnalyticalObject', () => { + it('creates an ANALYTICAL_OBJECT_SET action', () => { + const ao = { id: 'ao1' } + + expect(setAnalyticalObject(ao)).toEqual({ + type: types.ANALYTICAL_OBJECT_SET, + payload: ao, + }) + }) +}) + +describe('clearAnalyticalObject', () => { + it('creates an ANALYTICAL_OBJECT_CLEAR action', () => { + expect(clearAnalyticalObject()).toEqual({ + type: types.ANALYTICAL_OBJECT_CLEAR, + }) + }) +}) diff --git a/src/actions/__tests__/basemap.spec.js b/src/actions/__tests__/basemap.spec.js new file mode 100644 index 000000000..30dbe5eb6 --- /dev/null +++ b/src/actions/__tests__/basemap.spec.js @@ -0,0 +1,43 @@ +import * as types from '../../constants/actionTypes.js' +import { + selectBasemap, + toggleBasemapExpand, + toggleBasemapVisibility, + changeBasemapOpacity, +} from '../basemap.js' + +describe('selectBasemap', () => { + it('creates a BASEMAP_SELECTED action', () => { + const payload = { id: 'osm' } + + expect(selectBasemap(payload)).toEqual({ + type: types.BASEMAP_SELECTED, + payload, + }) + }) +}) + +describe('toggleBasemapExpand', () => { + it('creates a BASEMAP_TOGGLE_EXPAND action', () => { + expect(toggleBasemapExpand()).toEqual({ + type: types.BASEMAP_TOGGLE_EXPAND, + }) + }) +}) + +describe('toggleBasemapVisibility', () => { + it('creates a BASEMAP_TOGGLE_VISIBILITY action', () => { + expect(toggleBasemapVisibility()).toEqual({ + type: types.BASEMAP_TOGGLE_VISIBILITY, + }) + }) +}) + +describe('changeBasemapOpacity', () => { + it('creates a BASEMAP_CHANGE_OPACITY action', () => { + expect(changeBasemapOpacity(0.5)).toEqual({ + type: types.BASEMAP_CHANGE_OPACITY, + opacity: 0.5, + }) + }) +}) diff --git a/src/actions/__tests__/dataFilters.spec.js b/src/actions/__tests__/dataFilters.spec.js new file mode 100644 index 000000000..1873e36d5 --- /dev/null +++ b/src/actions/__tests__/dataFilters.spec.js @@ -0,0 +1,25 @@ +import * as types from '../../constants/actionTypes.js' +import { setDataFilter, clearDataFilter } from '../dataFilters.js' + +describe('setDataFilter', () => { + it('creates a DATA_FILTER_SET action', () => { + const filter = { operator: 'eq', value: 1 } + + expect(setDataFilter('layer1', 'field1', filter)).toEqual({ + type: types.DATA_FILTER_SET, + layerId: 'layer1', + fieldId: 'field1', + filter, + }) + }) +}) + +describe('clearDataFilter', () => { + it('creates a DATA_FILTER_CLEAR action', () => { + expect(clearDataFilter('layer1', 'field1')).toEqual({ + type: types.DATA_FILTER_CLEAR, + layerId: 'layer1', + fieldId: 'field1', + }) + }) +}) diff --git a/src/actions/__tests__/dataTable.spec.js b/src/actions/__tests__/dataTable.spec.js new file mode 100644 index 000000000..bc50b2763 --- /dev/null +++ b/src/actions/__tests__/dataTable.spec.js @@ -0,0 +1,32 @@ +import * as types from '../../constants/actionTypes.js' +import { + closeDataTable, + toggleDataTable, + resizeDataTable, +} from '../dataTable.js' + +describe('closeDataTable', () => { + it('creates a DATA_TABLE_CLOSE action', () => { + expect(closeDataTable()).toEqual({ + type: types.DATA_TABLE_CLOSE, + }) + }) +}) + +describe('toggleDataTable', () => { + it('creates a DATA_TABLE_TOGGLE action', () => { + expect(toggleDataTable('layer1')).toEqual({ + type: types.DATA_TABLE_TOGGLE, + id: 'layer1', + }) + }) +}) + +describe('resizeDataTable', () => { + it('creates a DATA_TABLE_RESIZE action', () => { + expect(resizeDataTable(300)).toEqual({ + type: types.DATA_TABLE_RESIZE, + height: 300, + }) + }) +}) diff --git a/src/actions/__tests__/download.spec.js b/src/actions/__tests__/download.spec.js new file mode 100644 index 000000000..bb6a7b232 --- /dev/null +++ b/src/actions/__tests__/download.spec.js @@ -0,0 +1,13 @@ +import * as types from '../../constants/actionTypes.js' +import { setDownloadConfig } from '../download.js' + +describe('setDownloadConfig', () => { + it('creates a DOWNLOAD_CONFIG_SET action', () => { + const payload = { format: 'png' } + + expect(setDownloadConfig(payload)).toEqual({ + type: types.DOWNLOAD_CONFIG_SET, + payload, + }) + }) +}) diff --git a/src/actions/__tests__/feature.spec.js b/src/actions/__tests__/feature.spec.js new file mode 100644 index 000000000..dc2203ac0 --- /dev/null +++ b/src/actions/__tests__/feature.spec.js @@ -0,0 +1,36 @@ +import * as types from '../../constants/actionTypes.js' +import { + highlightFeature, + setFeatureProfile, + closeFeatureProfile, +} from '../feature.js' + +describe('highlightFeature', () => { + it('creates a FEATURE_HIGHLIGHT action', () => { + const payload = { id: 'feat1' } + + expect(highlightFeature(payload)).toEqual({ + type: types.FEATURE_HIGHLIGHT, + payload, + }) + }) +}) + +describe('setFeatureProfile', () => { + it('creates a FEATURE_PROFILE_SET action', () => { + const payload = { id: 'feat1' } + + expect(setFeatureProfile(payload)).toEqual({ + type: types.FEATURE_PROFILE_SET, + payload, + }) + }) +}) + +describe('closeFeatureProfile', () => { + it('creates a FEATURE_PROFILE_CLOSE action', () => { + expect(closeFeatureProfile()).toEqual({ + type: types.FEATURE_PROFILE_CLOSE, + }) + }) +}) diff --git a/src/actions/__tests__/helpers.spec.js b/src/actions/__tests__/helpers.spec.js new file mode 100644 index 000000000..fbcc811ec --- /dev/null +++ b/src/actions/__tests__/helpers.spec.js @@ -0,0 +1,21 @@ +import { errorActionCreator } from '../helpers.js' + +describe('errorActionCreator', () => { + it('creates an action with the given type and stringified error', () => { + const action = errorActionCreator('SOME_ERROR')(new Error('Oops')) + + expect(action).toEqual({ + type: 'SOME_ERROR', + error: 'Error: Oops', + }) + }) + + it('stringifies non-Error values', () => { + const action = errorActionCreator('SOME_ERROR')('Something went wrong') + + expect(action).toEqual({ + type: 'SOME_ERROR', + error: 'Something went wrong', + }) + }) +}) diff --git a/src/actions/__tests__/interpretations.spec.js b/src/actions/__tests__/interpretations.spec.js new file mode 100644 index 000000000..b71ba62ee --- /dev/null +++ b/src/actions/__tests__/interpretations.spec.js @@ -0,0 +1,11 @@ +import * as types from '../../constants/actionTypes.js' +import { setInterpretation } from '../interpretations.js' + +describe('setInterpretation', () => { + it('creates an INTERPRETATION_SET action', () => { + expect(setInterpretation('int1')).toEqual({ + type: types.INTERPRETATION_SET, + payload: 'int1', + }) + }) +}) diff --git a/src/actions/__tests__/layerEdit.spec.js b/src/actions/__tests__/layerEdit.spec.js new file mode 100644 index 000000000..2ae909d06 --- /dev/null +++ b/src/actions/__tests__/layerEdit.spec.js @@ -0,0 +1,488 @@ +import * as types from '../../constants/actionTypes.js' +import { + addDimensionFilter, + removeDimensionFilter, + changeDimensionFilter, + addFilter, + removeFilter, + changeFilter, + setProgram, + setProgramStage, + setDataItem, + setStyleDataItem, + setOptionStyle, + setBooleanStyle, + setThematicMapType, + setClassification, + setColorScale, + setLegendDecimalPlaces, + setLegendIsolated, + setEventStatus, + setEventCoordinateField, + setFallbackCoordinateField, + setEventClustering, + setCountFeaturesWithoutCoordinates, + setCountEventsOutsideOrgUnits, + setEventPointRadius, + setEventPointColor, + setRelatedPointColor, + setRelatedPointRadius, + setRelationshipLineColor, + setOrganisationUnitGroupSet, + setOrganisationUnitColor, + setOrganisationUnitField, + setPeriodName, + setPeriodType, + setPeriods, + setStartDate, + setEndDate, + setBackupPeriodsDates, + setAggregationType, + setOrgUnits, + setOrgUnitMode, + setStyle, + setFilter, + setBand, + setLabels, + setLabelTemplate, + setLabelFontSize, + setLabelFontWeight, + setLabelFontStyle, + setLabelFontColor, + setBufferRadius, + setGeometryCentroid, + setRadiusLow, + setRadiusHigh, + setLegendSet, + setOrgUnitPath, + setTrackedEntityType, + setTrackedEntityRelationshipType, + setTrackedEntityRelationshipOutsideProgram, + setProgramStatus, + setFollowUpStatus, + setRenderingStrategy, + setNoDataLegend, + setUnclassifiedLegend, + setEarthEnginePeriod, + setFeatureStyle, + setLabelDataItem, +} from '../layerEdit.js' + +describe('layerEdit simple action creators', () => { + it.each([ + { + creator: addDimensionFilter, + args: [{ dimension: 'ou' }], + type: types.LAYER_EDIT_DIMENSION_FILTER_ADD, + rest: { filter: { dimension: 'ou' } }, + }, + { + creator: removeDimensionFilter, + args: [1], + type: types.LAYER_EDIT_DIMENSION_FILTER_REMOVE, + rest: { index: 1 }, + }, + { + creator: changeDimensionFilter, + args: [1, { dimension: 'ou' }], + type: types.LAYER_EDIT_DIMENSION_FILTER_CHANGE, + rest: { index: 1, filter: { dimension: 'ou' } }, + }, + { + creator: addFilter, + args: [{ dimension: 'dx' }], + type: types.LAYER_EDIT_FILTER_ADD, + rest: { filter: { dimension: 'dx' } }, + }, + { + creator: removeFilter, + args: [2], + type: types.LAYER_EDIT_FILTER_REMOVE, + rest: { index: 2 }, + }, + { + creator: changeFilter, + args: [2, { dimension: 'dx' }], + type: types.LAYER_EDIT_FILTER_CHANGE, + rest: { index: 2, filter: { dimension: 'dx' } }, + }, + { + creator: setProgram, + args: [{ id: 'prog1' }], + type: types.LAYER_EDIT_PROGRAM_SET, + rest: { program: { id: 'prog1' } }, + }, + { + creator: setProgramStage, + args: [{ id: 'stage1' }], + type: types.LAYER_EDIT_PROGRAM_STAGE_SET, + rest: { programStage: { id: 'stage1' } }, + }, + { + creator: setDataItem, + args: [{ id: 'de1' }, 'columns'], + type: types.LAYER_EDIT_DATA_ITEM_SET, + rest: { dataItem: { id: 'de1' }, dimension: 'columns' }, + }, + { + creator: setStyleDataItem, + args: [{ id: 'de1' }], + type: types.LAYER_EDIT_STYLE_DATA_ITEM_SET, + rest: { dataItem: { id: 'de1' } }, + }, + { + creator: setOptionStyle, + args: [[{ code: 'A' }]], + type: types.LAYER_EDIT_STYLE_DATA_ITEM_OPTIONS_SET, + rest: { options: [{ code: 'A' }] }, + }, + { + creator: setBooleanStyle, + args: ['true', '#fff'], + type: types.LAYER_EDIT_STYLE_DATA_ITEM_BOOLEAN_SET, + rest: { value: 'true', color: '#fff' }, + }, + { + creator: setThematicMapType, + args: ['CHOROPLETH'], + type: types.LAYER_EDIT_THEMATIC_MAP_TYPE_SET, + rest: { payload: 'CHOROPLETH' }, + }, + { + creator: setClassification, + args: ['equalIntervals'], + type: types.LAYER_EDIT_CLASSIFICATION_SET, + rest: { method: 'equalIntervals' }, + }, + { + creator: setColorScale, + args: [['#fff', '#000']], + type: types.LAYER_EDIT_COLOR_SCALE_SET, + rest: { colorScale: ['#fff', '#000'] }, + }, + { + creator: setLegendDecimalPlaces, + args: [2], + type: types.LAYER_EDIT_LEGEND_DECIMAL_PLACES_SET, + rest: { legendDecimalPlaces: 2 }, + }, + { + creator: setLegendIsolated, + args: [true], + type: types.LAYER_EDIT_LEGEND_ISOLATED_SET, + rest: { legendIsolated: true }, + }, + { + creator: setEventStatus, + args: ['ACTIVE'], + type: types.LAYER_EDIT_EVENT_STATUS_SET, + rest: { status: 'ACTIVE' }, + }, + { + creator: setEventCoordinateField, + args: ['field1', 'COORDINATE'], + type: types.LAYER_EDIT_EVENT_COORDINATE_FIELD_SET, + rest: { fieldId: 'field1', fieldType: 'COORDINATE' }, + }, + { + creator: setFallbackCoordinateField, + args: ['field1'], + type: types.LAYER_EDIT_FALLBACK_COORDINATE_FIELD_SET, + rest: { fieldId: 'field1' }, + }, + { + creator: setEventClustering, + args: [true], + type: types.LAYER_EDIT_EVENT_CLUSTERING_SET, + rest: { checked: true }, + }, + { + creator: setCountFeaturesWithoutCoordinates, + args: [true], + type: types.LAYER_EDIT_COUNT_FEATURES_WITHOUT_COORDS_SET, + rest: { checked: true }, + }, + { + creator: setCountEventsOutsideOrgUnits, + args: [true], + type: types.LAYER_EDIT_COUNT_EVENTS_OUTSIDE_OU_SET, + rest: { checked: true }, + }, + { + creator: setEventPointRadius, + args: [5], + type: types.LAYER_EDIT_EVENT_POINT_RADIUS_SET, + rest: { radius: 5 }, + }, + { + creator: setEventPointColor, + args: ['#fff'], + type: types.LAYER_EDIT_EVENT_POINT_COLOR_SET, + rest: { color: '#fff' }, + }, + { + creator: setRelatedPointColor, + args: ['#fff'], + type: types.LAYER_EDIT_RELATED_POINT_COLOR_SET, + rest: { color: '#fff' }, + }, + { + creator: setRelatedPointRadius, + args: [5], + type: types.LAYER_EDIT_RELATED_POINT_RADIUS_SET, + rest: { radius: 5 }, + }, + { + creator: setRelationshipLineColor, + args: ['#fff'], + type: types.LAYER_EDIT_RELATIONSHIP_LINE_COLOR_SET, + rest: { color: '#fff' }, + }, + { + creator: setOrganisationUnitGroupSet, + args: [{ id: 'ougs1' }], + type: types.LAYER_EDIT_ORGANISATION_UNIT_GROUP_SET, + rest: { organisationUnitGroupSet: { id: 'ougs1' } }, + }, + { + creator: setOrganisationUnitColor, + args: ['#fff'], + type: types.LAYER_EDIT_ORGANISATION_UNIT_COLOR_SET, + rest: { color: '#fff' }, + }, + { + creator: setOrganisationUnitField, + args: [{ id: 'field1' }], + type: types.LAYER_EDIT_ORGANISATION_UNIT_FIELD_SET, + rest: { payload: { id: 'field1' } }, + }, + { + creator: setPeriodName, + args: ['Last year'], + type: types.LAYER_EDIT_PERIOD_NAME_SET, + rest: { periodName: 'Last year' }, + }, + { + creator: setPeriodType, + args: ['RELATIVE', true], + type: types.LAYER_EDIT_PERIOD_TYPE_SET, + rest: { periodType: 'RELATIVE', keepPeriod: true }, + }, + { + creator: setPeriods, + args: [[{ id: '2021' }]], + type: types.LAYER_EDIT_PERIODS_SET, + rest: { periods: [{ id: '2021' }] }, + }, + { + creator: setStartDate, + args: ['2021-01-01'], + type: types.LAYER_EDIT_START_DATE_SET, + rest: { startDate: '2021-01-01' }, + }, + { + creator: setEndDate, + args: ['2021-12-31'], + type: types.LAYER_EDIT_END_DATE_SET, + rest: { endDate: '2021-12-31' }, + }, + { + creator: setBackupPeriodsDates, + args: [{ periods: [] }], + type: types.LAYER_EDIT_BACKUP_PERIODSDATES_SET, + rest: { backupPeriodsDates: { periods: [] } }, + }, + { + creator: setAggregationType, + args: ['SUM'], + type: types.LAYER_EDIT_AGGREGATION_TYPE_SET, + rest: { aggregationType: 'SUM' }, + }, + { + creator: setOrgUnits, + args: [{ id: 'ou1' }], + type: types.LAYER_EDIT_ORGANISATION_UNITS_SET, + rest: { payload: { id: 'ou1' } }, + }, + { + creator: setOrgUnitMode, + args: ['SELECT'], + type: types.LAYER_EDIT_ORGANISATION_UNIT_MODE_SET, + rest: { payload: 'SELECT' }, + }, + { + creator: setStyle, + args: [{ color: '#fff' }], + type: types.LAYER_EDIT_STYLE_SET, + rest: { payload: { color: '#fff' } }, + }, + { + creator: setFilter, + args: ['NDVI > 0.5'], + type: types.LAYER_EDIT_FILTER_SET, + rest: { filter: 'NDVI > 0.5' }, + }, + { + creator: setLabels, + args: [true], + type: types.LAYER_EDIT_LABELS_SET, + rest: { isChecked: true }, + }, + { + creator: setLabelTemplate, + args: ['{name}'], + type: types.LAYER_EDIT_LABEL_TEMPLATE, + rest: { template: '{name}' }, + }, + { + creator: setLabelFontSize, + args: [12], + type: types.LAYER_EDIT_LABEL_FONT_SIZE_SET, + rest: { size: 12 }, + }, + { + creator: setLabelFontWeight, + args: ['bold'], + type: types.LAYER_EDIT_LABEL_FONT_WEIGHT_SET, + rest: { weight: 'bold' }, + }, + { + creator: setLabelFontStyle, + args: ['italic'], + type: types.LAYER_EDIT_LABEL_FONT_STYLE_SET, + rest: { style: 'italic' }, + }, + { + creator: setLabelFontColor, + args: ['#fff'], + type: types.LAYER_EDIT_LABEL_FONT_COLOR_SET, + rest: { color: '#fff' }, + }, + { + creator: setBufferRadius, + args: [500], + type: types.LAYER_EDIT_BUFFER_RADIUS_SET, + rest: { radius: 500 }, + }, + { + creator: setGeometryCentroid, + args: [true], + type: types.LAYER_EDIT_GEOMETRY_CENTROIDS_SET, + rest: { payload: true }, + }, + { + creator: setRadiusLow, + args: [2], + type: types.LAYER_EDIT_RADIUS_LOW_SET, + rest: { radius: 2 }, + }, + { + creator: setRadiusHigh, + args: [10], + type: types.LAYER_EDIT_RADIUS_HIGH_SET, + rest: { radius: 10 }, + }, + { + creator: setLegendSet, + args: [{ id: 'ls1' }], + type: types.LAYER_EDIT_LEGEND_SET_SET, + rest: { legendSet: { id: 'ls1' } }, + }, + { + creator: setOrgUnitPath, + args: ['ou1', '/root/ou1'], + type: types.LAYER_EDIT_ORGANISATION_UNIT_PATH_SET, + rest: { id: 'ou1', path: '/root/ou1' }, + }, + { + creator: setTrackedEntityType, + args: [{ id: 'tet1' }], + type: types.LAYER_EDIT_TRACKED_ENTITY_TYPE_SET, + rest: { trackedEntityType: { id: 'tet1' } }, + }, + { + creator: setTrackedEntityRelationshipType, + args: [{ id: 'rt1' }], + type: types.LAYER_EDIT_TRACKED_ENTITY_RELATIONSHIP_TYPE_SET, + rest: { relationshipType: { id: 'rt1' } }, + }, + { + creator: setTrackedEntityRelationshipOutsideProgram, + args: [true], + type: types.LAYER_EDIT_TRACKED_ENTITY_RELATIONSHIP_OUTSIDE_PROGRAM_SET, + rest: { payload: true }, + }, + { + creator: setProgramStatus, + args: ['ACTIVE'], + type: types.LAYER_EDIT_PROGRAM_STATUS_SET, + rest: { payload: 'ACTIVE' }, + }, + { + creator: setFollowUpStatus, + args: [true], + type: types.LAYER_EDIT_FOLLOW_UP_SET, + rest: { payload: true }, + }, + { + creator: setRenderingStrategy, + args: ['SINGLE'], + type: types.LAYER_EDIT_RENDERING_STRATEGY_SET, + rest: { payload: 'SINGLE' }, + }, + { + creator: setNoDataLegend, + args: [true], + type: types.LAYER_EDIT_NO_DATA_LEGEND_SET, + rest: { payload: true }, + }, + { + creator: setUnclassifiedLegend, + args: [true], + type: types.LAYER_EDIT_UNCLASSIFIED_LEGEND_SET, + rest: { payload: true }, + }, + { + creator: setEarthEnginePeriod, + args: [{ id: 'period1' }], + type: types.LAYER_EDIT_EARTH_ENGINE_PERIOD_SET, + rest: { payload: { id: 'period1' } }, + }, + { + creator: setFeatureStyle, + args: [{ color: '#fff' }], + type: types.LAYER_EDIT_FEATURE_STYLE_SET, + rest: { payload: { color: '#fff' } }, + }, + { + creator: setLabelDataItem, + args: [{ id: 'de1' }], + type: types.LAYER_EDIT_LABEL_DATA_ITEM_ID_SET, + rest: { item: { id: 'de1' } }, + }, + ])( + '$creator.name creates the expected action', + ({ creator, args, type, rest }) => { + expect(creator(...args)).toEqual({ + type, + ...rest, + }) + } + ) +}) + +describe('setBand', () => { + it('uses the id of the given band object', () => { + expect(setBand({ id: 'B4' })).toEqual({ + type: types.LAYER_EDIT_BAND_SET, + payload: 'B4', + }) + }) + + it('uses the value directly when not given an object', () => { + expect(setBand('B4')).toEqual({ + type: types.LAYER_EDIT_BAND_SET, + payload: 'B4', + }) + }) +}) diff --git a/src/actions/__tests__/layerSources.spec.js b/src/actions/__tests__/layerSources.spec.js new file mode 100644 index 000000000..2afef874f --- /dev/null +++ b/src/actions/__tests__/layerSources.spec.js @@ -0,0 +1,35 @@ +import * as types from '../../constants/actionTypes.js' +import { + initLayerSources, + addLayerSource, + removeLayerSource, +} from '../layerSources.js' + +describe('initLayerSources', () => { + it('creates a LAYER_SOURCES_INIT action', () => { + const sources = ['src1', 'src2'] + + expect(initLayerSources(sources)).toEqual({ + type: types.LAYER_SOURCES_INIT, + payload: sources, + }) + }) +}) + +describe('addLayerSource', () => { + it('creates a LAYER_SOURCE_ADD action', () => { + expect(addLayerSource('src1')).toEqual({ + type: types.LAYER_SOURCE_ADD, + payload: 'src1', + }) + }) +}) + +describe('removeLayerSource', () => { + it('creates a LAYER_SOURCE_REMOVE action', () => { + expect(removeLayerSource('src1')).toEqual({ + type: types.LAYER_SOURCE_REMOVE, + payload: 'src1', + }) + }) +}) diff --git a/src/actions/__tests__/layers.spec.js b/src/actions/__tests__/layers.spec.js new file mode 100644 index 000000000..57b69a6f3 --- /dev/null +++ b/src/actions/__tests__/layers.spec.js @@ -0,0 +1,118 @@ +import * as types from '../../constants/actionTypes.js' +import { + addLayer, + removeLayer, + duplicateLayer, + editLayer, + cancelLayer, + updateLayer, + toggleLayerExpand, + toggleLayerVisibility, + changeLayerOpacity, + sortLayers, + setLayerLoading, +} from '../layers.js' + +describe('addLayer', () => { + it('creates a LAYER_ADD action', () => { + const config = { id: 'layer1' } + + expect(addLayer(config)).toEqual({ + type: types.LAYER_ADD, + payload: config, + }) + }) +}) + +describe('removeLayer', () => { + it('creates a LAYER_REMOVE action', () => { + expect(removeLayer('layer1')).toEqual({ + type: types.LAYER_REMOVE, + id: 'layer1', + }) + }) +}) + +describe('duplicateLayer', () => { + it('creates a LAYER_DUPLICATE action', () => { + expect(duplicateLayer('layer1')).toEqual({ + type: types.LAYER_DUPLICATE, + id: 'layer1', + }) + }) +}) + +describe('editLayer', () => { + it('creates a LAYER_EDIT action', () => { + const layer = { id: 'layer1' } + + expect(editLayer(layer)).toEqual({ + type: types.LAYER_EDIT, + payload: layer, + }) + }) +}) + +describe('cancelLayer', () => { + it('creates a LAYER_CANCEL action', () => { + expect(cancelLayer()).toEqual({ type: types.LAYER_CANCEL }) + }) +}) + +describe('updateLayer', () => { + it('creates a LAYER_UPDATE action', () => { + const layer = { id: 'layer1' } + + expect(updateLayer(layer)).toEqual({ + type: types.LAYER_UPDATE, + payload: layer, + }) + }) +}) + +describe('toggleLayerExpand', () => { + it('creates a LAYER_TOGGLE_EXPAND action', () => { + expect(toggleLayerExpand('layer1')).toEqual({ + type: types.LAYER_TOGGLE_EXPAND, + id: 'layer1', + }) + }) +}) + +describe('toggleLayerVisibility', () => { + it('creates a LAYER_TOGGLE_VISIBILITY action', () => { + expect(toggleLayerVisibility('layer1')).toEqual({ + type: types.LAYER_TOGGLE_VISIBILITY, + id: 'layer1', + }) + }) +}) + +describe('changeLayerOpacity', () => { + it('creates a LAYER_CHANGE_OPACITY action', () => { + expect(changeLayerOpacity('layer1', 0.5)).toEqual({ + type: types.LAYER_CHANGE_OPACITY, + id: 'layer1', + opacity: 0.5, + }) + }) +}) + +describe('sortLayers', () => { + it('creates a LAYER_SORT action', () => { + expect(sortLayers({ oldIndex: 0, newIndex: 2 })).toEqual({ + type: types.LAYER_SORT, + oldIndex: 0, + newIndex: 2, + }) + }) +}) + +describe('setLayerLoading', () => { + it('creates a LAYER_LOADING_SET action', () => { + expect(setLayerLoading('layer1')).toEqual({ + type: types.LAYER_LOADING_SET, + id: 'layer1', + }) + }) +}) diff --git a/src/actions/__tests__/map.spec.js b/src/actions/__tests__/map.spec.js new file mode 100644 index 000000000..161de5b8d --- /dev/null +++ b/src/actions/__tests__/map.spec.js @@ -0,0 +1,109 @@ +import * as types from '../../constants/actionTypes.js' +import { + newMap, + setMap, + setMapProps, + openCoordinatePopup, + closeCoordinatePopup, + openContextMenu, + closeContextMenu, + showEarthEngineValue, + clearAlerts, + syncMapPeriods, +} from '../map.js' + +describe('newMap', () => { + it('creates a MAP_NEW action', () => { + expect(newMap()).toEqual({ type: types.MAP_NEW }) + }) +}) + +describe('setMap', () => { + it('creates a MAP_SET action', () => { + const config = { id: 'map1' } + + expect(setMap(config)).toEqual({ + type: types.MAP_SET, + payload: config, + }) + }) +}) + +describe('setMapProps', () => { + it('creates a MAP_PROPS_SET action', () => { + const props = { zoom: 5 } + + expect(setMapProps(props)).toEqual({ + type: types.MAP_PROPS_SET, + payload: props, + }) + }) +}) + +describe('openCoordinatePopup', () => { + it('creates a MAP_COORDINATE_OPEN action', () => { + const coord = [1, 2] + + expect(openCoordinatePopup(coord)).toEqual({ + type: types.MAP_COORDINATE_OPEN, + payload: coord, + }) + }) +}) + +describe('closeCoordinatePopup', () => { + it('creates a MAP_COORDINATE_CLOSE action', () => { + expect(closeCoordinatePopup()).toEqual({ + type: types.MAP_COORDINATE_CLOSE, + }) + }) +}) + +describe('openContextMenu', () => { + it('creates a MAP_CONTEXT_MENU_OPEN action', () => { + const payload = { x: 1, y: 2 } + + expect(openContextMenu(payload)).toEqual({ + type: types.MAP_CONTEXT_MENU_OPEN, + payload, + }) + }) +}) + +describe('closeContextMenu', () => { + it('creates a MAP_CONTEXT_MENU_CLOSE action', () => { + expect(closeContextMenu()).toEqual({ + type: types.MAP_CONTEXT_MENU_CLOSE, + }) + }) +}) + +describe('showEarthEngineValue', () => { + it('creates a MAP_EARTH_ENGINE_VALUE_SHOW action', () => { + const coordinate = [1, 2] + + expect(showEarthEngineValue('layer1', coordinate)).toEqual({ + type: types.MAP_EARTH_ENGINE_VALUE_SHOW, + layerId: 'layer1', + coordinate, + }) + }) +}) + +describe('clearAlerts', () => { + it('creates a MAP_ALERTS_CLEAR action', () => { + expect(clearAlerts()).toEqual({ type: types.MAP_ALERTS_CLEAR }) + }) +}) + +describe('syncMapPeriods', () => { + it('creates a MAP_PERIODS_SYNC action', () => { + const periods = [{ id: '2021' }] + + expect(syncMapPeriods(periods, 'SINGLE')).toEqual({ + type: types.MAP_PERIODS_SYNC, + periods, + renderingStrategy: 'SINGLE', + }) + }) +}) diff --git a/src/actions/__tests__/orgUnits.spec.js b/src/actions/__tests__/orgUnits.spec.js new file mode 100644 index 000000000..a18ac1ceb --- /dev/null +++ b/src/actions/__tests__/orgUnits.spec.js @@ -0,0 +1,19 @@ +import * as types from '../../constants/actionTypes.js' +import { setOrgUnitProfile, closeOrgUnitProfile } from '../orgUnits.js' + +describe('setOrgUnitProfile', () => { + it('creates an ORGANISATION_UNIT_PROFILE_SET action', () => { + expect(setOrgUnitProfile('ou1')).toEqual({ + type: types.ORGANISATION_UNIT_PROFILE_SET, + payload: 'ou1', + }) + }) +}) + +describe('closeOrgUnitProfile', () => { + it('creates an ORGANISATION_UNIT_PROFILE_CLOSE action', () => { + expect(closeOrgUnitProfile()).toEqual({ + type: types.ORGANISATION_UNIT_PROFILE_CLOSE, + }) + }) +}) diff --git a/src/actions/__tests__/ui.spec.js b/src/actions/__tests__/ui.spec.js new file mode 100644 index 000000000..feb074ef8 --- /dev/null +++ b/src/actions/__tests__/ui.spec.js @@ -0,0 +1,53 @@ +import * as types from '../../constants/actionTypes.js' +import { + openLayersPanel, + closeLayersPanel, + openInterpretationsPanel, + closeInterpretationsPanel, + openDownloadMode, + closeDownloadMode, + layersSortingStart, + layersSortingEnd, +} from '../ui.js' + +describe('ui actions', () => { + it('creates a LAYERS_PANEL_OPEN action', () => { + expect(openLayersPanel()).toEqual({ type: types.LAYERS_PANEL_OPEN }) + }) + + it('creates a LAYERS_PANEL_CLOSE action', () => { + expect(closeLayersPanel()).toEqual({ type: types.LAYERS_PANEL_CLOSE }) + }) + + it('creates an INTERPRETATIONS_PANEL_OPEN action', () => { + expect(openInterpretationsPanel()).toEqual({ + type: types.INTERPRETATIONS_PANEL_OPEN, + }) + }) + + it('creates an INTERPRETATIONS_PANEL_CLOSE action', () => { + expect(closeInterpretationsPanel()).toEqual({ + type: types.INTERPRETATIONS_PANEL_CLOSE, + }) + }) + + it('creates a DOWNLOAD_MODE_OPEN action', () => { + expect(openDownloadMode()).toEqual({ type: types.DOWNLOAD_MODE_OPEN }) + }) + + it('creates a DOWNLOAD_MODE_CLOSE action', () => { + expect(closeDownloadMode()).toEqual({ + type: types.DOWNLOAD_MODE_CLOSE, + }) + }) + + it('creates a LAYERS_SORTING_START action', () => { + expect(layersSortingStart()).toEqual({ + type: types.LAYERS_SORTING_START, + }) + }) + + it('creates a LAYERS_SORTING_END action', () => { + expect(layersSortingEnd()).toEqual({ type: types.LAYERS_SORTING_END }) + }) +}) diff --git a/src/reducers/__tests__/aggregations.spec.js b/src/reducers/__tests__/aggregations.spec.js new file mode 100644 index 000000000..cacf8f65e --- /dev/null +++ b/src/reducers/__tests__/aggregations.spec.js @@ -0,0 +1,59 @@ +import * as types from '../../constants/actionTypes.js' +import aggregations from '../aggregations.js' + +describe('aggregations reducer', () => { + it('returns an empty object by default', () => { + expect(aggregations(undefined, {})).toEqual({}) + }) + + it('merges the payload into the state', () => { + const result = aggregations( + { layer1: 'sum' }, + { + type: types.AGGREGATIONS_SET, + payload: { layer2: 'avg' }, + } + ) + + expect(result).toEqual({ layer1: 'sum', layer2: 'avg' }) + }) + + it.each([types.MAP_NEW, types.MAP_SET])( + 'resets to an empty object on %s', + (type) => { + expect(aggregations({ layer1: 'sum' }, { type })).toEqual({}) + } + ) + + it('clears the aggregation for a removed layer', () => { + const result = aggregations( + { layer1: 'sum', layer2: 'avg' }, + { type: types.LAYER_REMOVE, id: 'layer1' } + ) + + expect(result).toEqual({ layer1: undefined, layer2: 'avg' }) + }) + + it('clears the aggregation for an updated layer', () => { + const result = aggregations( + { layer1: 'sum', layer2: 'avg' }, + { type: types.LAYER_UPDATE, payload: { id: 'layer1' } } + ) + + expect(result).toEqual({ layer1: undefined, layer2: 'avg' }) + }) + + it('returns the current state when the affected layer has no aggregation set', () => { + const state = { layer2: 'avg' } + + expect( + aggregations(state, { type: types.LAYER_REMOVE, id: 'layer1' }) + ).toBe(state) + }) + + it('returns the current state for unknown actions', () => { + const state = { layer1: 'sum' } + + expect(aggregations(state, { type: 'UNKNOWN' })).toBe(state) + }) +}) diff --git a/src/reducers/__tests__/analyticalObject.spec.js b/src/reducers/__tests__/analyticalObject.spec.js new file mode 100644 index 000000000..7d5dc3e52 --- /dev/null +++ b/src/reducers/__tests__/analyticalObject.spec.js @@ -0,0 +1,34 @@ +import * as types from '../../constants/actionTypes.js' +import analyticalObject from '../analyticalObject.js' + +describe('analyticalObject reducer', () => { + it('returns null by default', () => { + expect(analyticalObject(undefined, {})).toBe(null) + }) + + it('sets the analytical object', () => { + const payload = { id: 'ao1' } + + expect( + analyticalObject(null, { + type: types.ANALYTICAL_OBJECT_SET, + payload, + }) + ).toBe(payload) + }) + + it('clears the analytical object', () => { + expect( + analyticalObject( + { id: 'ao1' }, + { type: types.ANALYTICAL_OBJECT_CLEAR } + ) + ).toBe(null) + }) + + it('returns the current state for unknown actions', () => { + const state = { id: 'ao1' } + + expect(analyticalObject(state, { type: 'UNKNOWN' })).toBe(state) + }) +}) diff --git a/src/reducers/__tests__/contextMenu.spec.js b/src/reducers/__tests__/contextMenu.spec.js new file mode 100644 index 000000000..75f55800a --- /dev/null +++ b/src/reducers/__tests__/contextMenu.spec.js @@ -0,0 +1,31 @@ +import * as types from '../../constants/actionTypes.js' +import contextMenu from '../contextMenu.js' + +describe('contextMenu reducer', () => { + it('returns null by default', () => { + expect(contextMenu(undefined, {})).toBe(null) + }) + + it('opens the context menu with the given payload', () => { + const payload = { x: 1, y: 2 } + + expect( + contextMenu(null, { + type: types.MAP_CONTEXT_MENU_OPEN, + payload, + }) + ).toBe(payload) + }) + + it('closes the context menu', () => { + expect( + contextMenu({ x: 1, y: 2 }, { type: types.MAP_CONTEXT_MENU_CLOSE }) + ).toBe(null) + }) + + it('returns the current state for unknown actions', () => { + const state = { x: 1, y: 2 } + + expect(contextMenu(state, { type: 'UNKNOWN' })).toBe(state) + }) +}) diff --git a/src/reducers/__tests__/dataTable.spec.js b/src/reducers/__tests__/dataTable.spec.js new file mode 100644 index 000000000..2dcf5180b --- /dev/null +++ b/src/reducers/__tests__/dataTable.spec.js @@ -0,0 +1,52 @@ +import * as types from '../../constants/actionTypes.js' +import dataTable from '../dataTable.js' + +describe('dataTable reducer', () => { + it('returns null by default', () => { + expect(dataTable(undefined, {})).toBe(null) + }) + + it.each([ + types.DATA_TABLE_CLOSE, + types.MAP_NEW, + types.MAP_SET, + types.DOWNLOAD_MODE_CLOSE, + types.DOWNLOAD_MODE_OPEN, + ])('clears the open data table on %s', (type) => { + expect(dataTable('layer1', { type })).toBe(null) + }) + + it('closes the data table when toggling the currently open layer', () => { + expect( + dataTable('layer1', { + type: types.DATA_TABLE_TOGGLE, + id: 'layer1', + }) + ).toBe(null) + }) + + it('opens the data table when toggling a different layer', () => { + expect( + dataTable('layer1', { + type: types.DATA_TABLE_TOGGLE, + id: 'layer2', + }) + ).toBe('layer2') + }) + + it('clears the open data table when its layer is removed', () => { + expect( + dataTable('layer1', { type: types.LAYER_REMOVE, id: 'layer1' }) + ).toBe(null) + }) + + it('keeps the open data table when a different layer is removed', () => { + expect( + dataTable('layer1', { type: types.LAYER_REMOVE, id: 'layer2' }) + ).toBe('layer1') + }) + + it('returns the current state for unknown actions', () => { + expect(dataTable('layer1', { type: 'UNKNOWN' })).toBe('layer1') + }) +}) diff --git a/src/reducers/__tests__/download.spec.js b/src/reducers/__tests__/download.spec.js new file mode 100644 index 000000000..ad7084f55 --- /dev/null +++ b/src/reducers/__tests__/download.spec.js @@ -0,0 +1,37 @@ +import * as types from '../../constants/actionTypes.js' +import download from '../download.js' + +const defaultState = { + showName: true, + showDescription: true, + showLegend: true, + showInLegend: [], + showOverviewMap: true, + hasOverviewMapSpace: true, + showNorthArrow: true, + northArrowPosition: 'bottomright', + includeMargins: true, +} + +describe('download reducer', () => { + it('returns the default state by default', () => { + expect(download(undefined, {})).toEqual(defaultState) + }) + + it('merges the payload into the state', () => { + const result = download(defaultState, { + type: types.DOWNLOAD_CONFIG_SET, + payload: { showName: false, format: 'png' }, + }) + + expect(result).toEqual({ + ...defaultState, + showName: false, + format: 'png', + }) + }) + + it('returns the current state for unknown actions', () => { + expect(download(defaultState, { type: 'UNKNOWN' })).toBe(defaultState) + }) +}) diff --git a/src/reducers/__tests__/feature.spec.js b/src/reducers/__tests__/feature.spec.js new file mode 100644 index 000000000..cdc543e38 --- /dev/null +++ b/src/reducers/__tests__/feature.spec.js @@ -0,0 +1,35 @@ +import * as types from '../../constants/actionTypes.js' +import feature from '../feature.js' + +describe('feature reducer', () => { + it('returns null by default', () => { + expect(feature(undefined, {})).toBe(null) + }) + + it('replaces state with a copy of the highlighted feature', () => { + const payload = { id: 'feat1' } + + const result = feature(null, { + type: types.FEATURE_HIGHLIGHT, + payload, + }) + + expect(result).toEqual(payload) + expect(result).not.toBe(payload) + }) + + it('returns null when highlighting a falsy feature', () => { + const result = feature( + { id: 'feat1' }, + { type: types.FEATURE_HIGHLIGHT, payload: null } + ) + + expect(result).toBe(null) + }) + + it('returns the current state for unknown actions', () => { + const state = { id: 'feat1' } + + expect(feature(state, { type: 'UNKNOWN' })).toBe(state) + }) +}) diff --git a/src/reducers/__tests__/featureProfile.spec.js b/src/reducers/__tests__/featureProfile.spec.js new file mode 100644 index 000000000..8f9dd14ce --- /dev/null +++ b/src/reducers/__tests__/featureProfile.spec.js @@ -0,0 +1,35 @@ +import * as types from '../../constants/actionTypes.js' +import featureProfile from '../featureProfile.js' + +describe('featureProfile reducer', () => { + it('returns null by default', () => { + expect(featureProfile(undefined, {})).toBe(null) + }) + + it('sets the feature profile', () => { + const payload = { id: 'feat1' } + + expect( + featureProfile(null, { + type: types.FEATURE_PROFILE_SET, + payload, + }) + ).toBe(payload) + }) + + it.each([ + types.FEATURE_PROFILE_CLOSE, + types.ORGANISATION_UNIT_PROFILE_SET, + types.INTERPRETATIONS_PANEL_OPEN, + types.MAP_NEW, + types.MAP_SET, + ])('clears the feature profile on %s', (type) => { + expect(featureProfile({ id: 'feat1' }, { type })).toBe(null) + }) + + it('returns the current state for unknown actions', () => { + const state = { id: 'feat1' } + + expect(featureProfile(state, { type: 'UNKNOWN' })).toBe(state) + }) +}) diff --git a/src/reducers/__tests__/interpretation.spec.js b/src/reducers/__tests__/interpretation.spec.js new file mode 100644 index 000000000..5be6cf12f --- /dev/null +++ b/src/reducers/__tests__/interpretation.spec.js @@ -0,0 +1,23 @@ +import * as types from '../../constants/actionTypes.js' +import interpretation from '../interpretation.js' + +describe('interpretation reducer', () => { + it('returns an empty object by default', () => { + expect(interpretation(undefined, {})).toEqual({}) + }) + + it('sets the interpretation id', () => { + const result = interpretation( + { id: 'old' }, + { type: types.INTERPRETATION_SET, payload: 'int1' } + ) + + expect(result).toEqual({ id: 'int1' }) + }) + + it('returns the current state for unknown actions', () => { + const state = { id: 'int1' } + + expect(interpretation(state, { type: 'UNKNOWN' })).toBe(state) + }) +}) diff --git a/src/reducers/__tests__/layerEdit.spec.js b/src/reducers/__tests__/layerEdit.spec.js index b928a43ec..fd825639f 100644 --- a/src/reducers/__tests__/layerEdit.spec.js +++ b/src/reducers/__tests__/layerEdit.spec.js @@ -1,34 +1,1326 @@ import * as types from '../../constants/actionTypes.js' +import { EVENT_STATUS_ALL } from '../../constants/eventStatuses.js' +import { + CLASSIFICATION_SINGLE_COLOR, + CLASSIFICATION_EQUAL_INTERVALS, + CLASSIFICATION_PREDEFINED, + THEMATIC_CHOROPLETH, + EE_BUFFER, + NONE, +} from '../../constants/layers.js' +import { START_END_DATES } from '../../constants/periods.js' +import { + setDataItemInColumns, + setOrgUnitPathInRows, + setFiltersFromPeriods, + removePeriodFromFilters, + changeDimensionInFilters, + removeDimensionFromFilters, +} from '../../util/analytics.js' import layerEdit from '../layerEdit.js' describe('layerEdit reducer', () => { - it('keeps countEventsOutsideOrgUnits and countFeaturesWithoutCoordinates when the program changes', () => { - const state = { - countEventsOutsideOrgUnits: true, - countFeaturesWithoutCoordinates: true, - } + it('returns null by default', () => { + expect(layerEdit(undefined, { type: 'UNKNOWN' })).toBe(null) + }) + + describe('LAYER_EDIT / LAYER_CANCEL', () => { + it('replaces state with the layer payload and strips the img field', () => { + const payload = { id: 'layer1', img: 'thumb.png', name: 'Layer 1' } + + const result = layerEdit(null, { + type: types.LAYER_EDIT, + payload, + }) + + expect(result).toBe(payload) + expect(result).toEqual({ id: 'layer1', name: 'Layer 1' }) + }) + + it('clears the state on cancel', () => { + expect( + layerEdit({ id: 'layer1' }, { type: types.LAYER_CANCEL }) + ).toBe(null) + }) + }) + + describe('LAYER_EDIT_PROGRAM_SET / LAYER_EDIT_PROGRAM_STAGE_SET', () => { + it('keeps countEventsOutsideOrgUnits and countFeaturesWithoutCoordinates when the program changes', () => { + const state = { + countEventsOutsideOrgUnits: true, + countFeaturesWithoutCoordinates: true, + } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_PROGRAM_SET, + program: { id: 'prog1' }, + }) + + expect(result.countEventsOutsideOrgUnits).toBe(true) + expect(result.countFeaturesWithoutCoordinates).toBe(true) + }) + + it('keeps countEventsOutsideOrgUnits and countFeaturesWithoutCoordinates when the program stage changes', () => { + const state = { + countEventsOutsideOrgUnits: true, + countFeaturesWithoutCoordinates: true, + } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_PROGRAM_STAGE_SET, + programStage: { id: 'stage1' }, + }) + + expect(result.countEventsOutsideOrgUnits).toBe(true) + expect(result.countFeaturesWithoutCoordinates).toBe(true) + }) + + it('resets columns/programStage/styleDataItem/labelDataItem when the program changes', () => { + const state = { + columns: [{ dimension: 'dx' }], + programStage: { id: 'stage1' }, + styleDataItem: { id: 'sd1' }, + labelDataItem: { id: 'ld1' }, + } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_PROGRAM_SET, + program: { id: 'prog1' }, + }) + + expect(result).toEqual({ + ...state, + program: { id: 'prog1' }, + columns: [], + programStage: null, + styleDataItem: null, + labelDataItem: null, + }) + }) + + it('sets program to null when the program is cleared', () => { + const result = layerEdit( + {}, + { type: types.LAYER_EDIT_PROGRAM_SET, program: null } + ) + + expect(result.program).toBe(null) + }) + + it('resets columns/styleDataItem/labelDataItem when the program stage changes', () => { + const state = { + columns: [{ dimension: 'dx' }], + styleDataItem: { id: 'sd1' }, + labelDataItem: { id: 'ld1' }, + } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_PROGRAM_STAGE_SET, + programStage: { id: 'stage1' }, + }) + + expect(result).toEqual({ + ...state, + programStage: { id: 'stage1' }, + columns: [], + styleDataItem: null, + labelDataItem: null, + }) + }) + }) + + describe('LAYER_EDIT_DATA_ITEM_SET', () => { + it('sets columns from the data item and dimension, and clears the layer name', () => { + const dataItem = { id: 'de1', name: 'Data element 1' } + + const result = layerEdit( + { name: 'Old name' }, + { + type: types.LAYER_EDIT_DATA_ITEM_SET, + dataItem, + dimension: 'dataElement', + } + ) + + expect(result).toEqual({ + name: null, + columns: setDataItemInColumns(dataItem, 'dataElement'), + }) + }) + }) + + describe('period fields', () => { + it('sets the period name', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_PERIOD_NAME_SET, + periodName: 'Last year', + } + ) + + expect(result.periodName).toBe('Last year') + }) + + describe('LAYER_EDIT_PERIOD_TYPE_SET', () => { + it('clears the period filter when keepPeriod is false', () => { + const state = { + periodType: 'FIXED', + filters: [ + { dimension: 'pe', items: [] }, + { dimension: 'dx123', items: [] }, + ], + } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_PERIOD_TYPE_SET, + periodType: { value: 'RELATIVE' }, + keepPeriod: false, + }) + + expect(result.periodType).toBe('RELATIVE') + expect(result.filters).toEqual( + removePeriodFromFilters(state.filters) + ) + }) + + it('keeps the existing filters when keepPeriod is true', () => { + const state = { + periodType: 'FIXED', + filters: [{ dimension: 'pe', items: [] }], + } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_PERIOD_TYPE_SET, + periodType: { value: 'RELATIVE' }, + keepPeriod: true, + }) + + expect(result.filters).toBe(state.filters) + }) + }) + + describe('LAYER_EDIT_PERIODS_SET', () => { + it('sets filters from the given periods', () => { + const state = { filters: [] } + const periods = [{ id: '2021' }, { id: '2022' }] + + const result = layerEdit(state, { + type: types.LAYER_EDIT_PERIODS_SET, + periods, + }) + + expect(result.filters).toEqual( + setFiltersFromPeriods(state.filters, periods) + ) + }) + + it('clears filters when the only period is the START_END_DATES sentinel', () => { + const state = { filters: [{ dimension: 'pe', items: [] }] } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_PERIODS_SET, + periods: [{ id: START_END_DATES }], + }) + + expect(result.filters).toEqual([]) + }) + }) + + it('sets the periods/dates backup', () => { + const backup = { periods: [{ id: '2021' }] } + + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_BACKUP_PERIODSDATES_SET, + backupPeriodsDates: backup, + } + ) + + expect(result.backupPeriodsDates).toBe(backup) + }) + + it('sets the rendering strategy', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_RENDERING_STRATEGY_SET, + payload: 'SINGLE', + } + ) + + expect(result.renderingStrategy).toBe('SINGLE') + }) + + it('sets the start date', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_START_DATE_SET, + startDate: '2021-01-01', + } + ) + + expect(result.startDate).toBe('2021-01-01') + }) + + it('sets the end date', () => { + const result = layerEdit( + {}, + { type: types.LAYER_EDIT_END_DATE_SET, endDate: '2021-12-31' } + ) + + expect(result.endDate).toBe('2021-12-31') + }) + }) + + describe('LAYER_EDIT_AGGREGATION_TYPE_SET', () => { + it('deletes aggregationType when set to DEFAULT', () => { + const result = layerEdit( + { aggregationType: 'SUM' }, + { + type: types.LAYER_EDIT_AGGREGATION_TYPE_SET, + aggregationType: 'DEFAULT', + } + ) + + expect(result.aggregationType).toBeUndefined() + }) + + it('sets aggregationType for any other value', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_AGGREGATION_TYPE_SET, + aggregationType: 'SUM', + } + ) + + expect(result.aggregationType).toBe('SUM') + }) + }) + + describe('dimension filters', () => { + it('adds a dimension filter', () => { + const result = layerEdit( + { filters: [{ dimension: 'ou' }] }, + { + type: types.LAYER_EDIT_DIMENSION_FILTER_ADD, + filter: { dimension: 'dx1' }, + } + ) + + expect(result.filters).toEqual([ + { dimension: 'ou' }, + { dimension: 'dx1' }, + ]) + }) + + it('adds a default empty filter when none is given', () => { + const result = layerEdit( + {}, + { type: types.LAYER_EDIT_DIMENSION_FILTER_ADD } + ) + + expect(result.filters).toEqual([{ dimension: null }]) + }) + + it('removes a dimension filter by index', () => { + const filters = [{ dimension: 'dx1' }, { dimension: 'dx2' }] + + const result = layerEdit( + { filters }, + { type: types.LAYER_EDIT_DIMENSION_FILTER_REMOVE, index: 0 } + ) + + expect(result.filters).toEqual( + removeDimensionFromFilters(filters, 0) + ) + }) + + it('changes a dimension filter by index', () => { + const filters = [{ dimension: 'dx1' }] + const newFilter = { dimension: 'dx1', items: [{ id: 'a' }] } + + const result = layerEdit( + { filters }, + { + type: types.LAYER_EDIT_DIMENSION_FILTER_CHANGE, + index: 0, + filter: newFilter, + } + ) + + expect(result.filters).toEqual( + changeDimensionInFilters(filters, 0, newFilter) + ) + }) + }) + + describe('event filters (columns)', () => { + it('adds an event filter column', () => { + const result = layerEdit( + { columns: [{ dimension: 'dx' }] }, + { + type: types.LAYER_EDIT_FILTER_ADD, + filter: { dimension: 'de1', filter: 'GT:5' }, + } + ) + + expect(result.columns).toEqual([ + { dimension: 'dx' }, + { dimension: 'de1', filter: 'GT:5' }, + ]) + }) + + it('adds a default empty filter column when none is given', () => { + const result = layerEdit( + { columns: [] }, + { type: types.LAYER_EDIT_FILTER_ADD } + ) + + expect(result.columns).toEqual([ + { dimension: null, name: null, filter: null }, + ]) + }) + + describe('LAYER_EDIT_FILTER_REMOVE', () => { + it('removes a filter row by index, recompacting combined filters', () => { + const columns = [{ dimension: 'de1', filter: 'GT:5:LT:10' }] + + const result = layerEdit( + { columns }, + { type: types.LAYER_EDIT_FILTER_REMOVE, index: 0 } + ) + + expect(result.columns).toEqual([ + { dimension: 'de1', filter: 'LT:10' }, + ]) + }) + + it('returns state unchanged when the index is out of range', () => { + const state = { + columns: [{ dimension: 'de1', filter: 'GT:5' }], + } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_FILTER_REMOVE, + index: 5, + }) + + expect(result).toBe(state) + }) + }) + + describe('LAYER_EDIT_FILTER_CHANGE', () => { + it('replaces a filter row by index, recompacting combined filters', () => { + const columns = [{ dimension: 'de1', filter: 'GT:5' }] + + const result = layerEdit( + { columns }, + { + type: types.LAYER_EDIT_FILTER_CHANGE, + index: 0, + filter: { dimension: 'de1', filter: 'LT:20' }, + } + ) + + expect(result.columns).toEqual([ + { dimension: 'de1', filter: 'LT:20' }, + ]) + }) + + it('returns state unchanged when the index is out of range', () => { + const state = { + columns: [{ dimension: 'de1', filter: 'GT:5' }], + } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_FILTER_CHANGE, + index: 5, + filter: {}, + }) + + expect(result).toBe(state) + }) + }) + }) + + describe('thematic style', () => { + it('sets the style data item', () => { + const dataItem = { id: 'de1' } + + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_STYLE_DATA_ITEM_SET, + dataItem, + } + ) + + expect(result.styleDataItem).toBe(dataItem) + }) + + it('sets option-set options and clears method/classes/colorScale', () => { + const state = { + method: CLASSIFICATION_EQUAL_INTERVALS, + classes: 3, + colorScale: ['a'], + styleDataItem: { optionSet: { options: [] } }, + } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_STYLE_DATA_ITEM_OPTIONS_SET, + options: [{ code: 'A' }], + }) + + expect(result.styleDataItem.optionSet.options).toEqual([ + { code: 'A' }, + ]) + expect(result.method).toBeUndefined() + expect(result.classes).toBeUndefined() + expect(result.colorScale).toBeUndefined() + }) + + it('sets a boolean style value and clears method/classes/colorScale', () => { + const state = { + method: CLASSIFICATION_EQUAL_INTERVALS, + classes: 3, + colorScale: ['a'], + styleDataItem: { values: {} }, + } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_STYLE_DATA_ITEM_BOOLEAN_SET, + value: 'true', + color: '#fff', + }) + + expect(result.styleDataItem.values).toEqual({ true: '#fff' }) + expect(result.method).toBeUndefined() + expect(result.classes).toBeUndefined() + expect(result.colorScale).toBeUndefined() + }) + + describe('LAYER_EDIT_THEMATIC_MAP_TYPE_SET', () => { + it('clears method/colorScale/classes when switching to choropleth from single color', () => { + const state = { + method: CLASSIFICATION_SINGLE_COLOR, + colorScale: ['a'], + classes: 1, + } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_THEMATIC_MAP_TYPE_SET, + payload: THEMATIC_CHOROPLETH, + }) + + expect(result.thematicMapType).toBe(THEMATIC_CHOROPLETH) + expect(result.method).toBeUndefined() + expect(result.colorScale).toBeUndefined() + expect(result.classes).toBeUndefined() + }) + + it('keeps method/colorScale/classes when the target type is not choropleth', () => { + const state = { + method: CLASSIFICATION_SINGLE_COLOR, + colorScale: ['a'], + classes: 1, + } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_THEMATIC_MAP_TYPE_SET, + payload: 'BUBBLE', + }) + + expect(result.method).toBe(CLASSIFICATION_SINGLE_COLOR) + expect(result.colorScale).toEqual(['a']) + expect(result.classes).toBe(1) + }) + + it('keeps method/colorScale/classes when the previous method was not single color', () => { + const state = { + method: CLASSIFICATION_EQUAL_INTERVALS, + colorScale: ['a'], + classes: 1, + } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_THEMATIC_MAP_TYPE_SET, + payload: THEMATIC_CHOROPLETH, + }) + + expect(result.method).toBe(CLASSIFICATION_EQUAL_INTERVALS) + expect(result.colorScale).toEqual(['a']) + expect(result.classes).toBe(1) + }) + }) + + describe('LAYER_EDIT_CLASSIFICATION_SET', () => { + it('keeps colorScale/classes and clears legendSet for a valid non-predefined method', () => { + const state = { + colorScale: ['a'], + classes: 1, + legendSet: { id: 'ls1' }, + legendDecimalPlaces: 2, + } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_CLASSIFICATION_SET, + method: CLASSIFICATION_EQUAL_INTERVALS, + }) + + expect(result.method).toBe(CLASSIFICATION_EQUAL_INTERVALS) + expect(result.colorScale).toEqual(['a']) + expect(result.classes).toBe(1) + expect(result.legendSet).toBeUndefined() + expect(result.legendDecimalPlaces).toBe(2) + }) + + it('clears colorScale/classes when switching away from single color', () => { + const state = { + method: CLASSIFICATION_SINGLE_COLOR, + colorScale: ['a'], + classes: 1, + } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_CLASSIFICATION_SET, + method: CLASSIFICATION_EQUAL_INTERVALS, + }) + + expect(result.colorScale).toBeUndefined() + expect(result.classes).toBeUndefined() + }) + + it('clears colorScale/classes/legendDecimalPlaces and keeps legendSet for predefined classification', () => { + const state = { + colorScale: ['a'], + classes: 1, + legendSet: { id: 'ls1' }, + legendDecimalPlaces: 2, + } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_CLASSIFICATION_SET, + method: CLASSIFICATION_PREDEFINED, + }) + + expect(result.colorScale).toBeUndefined() + expect(result.classes).toBeUndefined() + expect(result.legendDecimalPlaces).toBeUndefined() + expect(result.legendSet).toEqual({ id: 'ls1' }) + }) + + it('clears styleDataItem.optionSet when present', () => { + const state = { + styleDataItem: { optionSet: { options: [] } }, + } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_CLASSIFICATION_SET, + method: CLASSIFICATION_EQUAL_INTERVALS, + }) + + expect(result.styleDataItem.optionSet).toBeUndefined() + }) + }) + + it('sets the color scale and derives classes from its length', () => { + const result = layerEdit( + { styleDataItem: { optionSet: {} } }, + { + type: types.LAYER_EDIT_COLOR_SCALE_SET, + colorScale: ['a', 'b', 'c'], + } + ) + + expect(result.colorScale).toEqual(['a', 'b', 'c']) + expect(result.classes).toBe(3) + expect(result.styleDataItem.optionSet).toBeUndefined() + }) + + it('sets the color scale without touching styleDataItem when there is none', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_COLOR_SCALE_SET, + colorScale: ['a', 'b'], + } + ) + + expect(result.colorScale).toEqual(['a', 'b']) + expect(result.classes).toBe(2) + expect(result.styleDataItem).toBeUndefined() + }) + + describe('LAYER_EDIT_LEGEND_DECIMAL_PLACES_SET', () => { + it('sets legendDecimalPlaces', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_LEGEND_DECIMAL_PLACES_SET, + legendDecimalPlaces: 2, + } + ) + + expect(result.legendDecimalPlaces).toBe(2) + }) + + it('deletes legendDecimalPlaces when set to undefined', () => { + const result = layerEdit( + { legendDecimalPlaces: 2 }, + { + type: types.LAYER_EDIT_LEGEND_DECIMAL_PLACES_SET, + legendDecimalPlaces: undefined, + } + ) - const result = layerEdit(state, { - type: types.LAYER_EDIT_PROGRAM_SET, - program: { id: 'prog1' }, + expect(result.legendDecimalPlaces).toBeUndefined() + }) }) - expect(result.countEventsOutsideOrgUnits).toBe(true) - expect(result.countFeaturesWithoutCoordinates).toBe(true) + describe('LAYER_EDIT_LEGEND_ISOLATED_SET', () => { + it('sets legendIsolated when truthy', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_LEGEND_ISOLATED_SET, + legendIsolated: true, + } + ) + + expect(result.legendIsolated).toBe(true) + }) + + it('deletes legendIsolated when falsy', () => { + const result = layerEdit( + { legendIsolated: true }, + { + type: types.LAYER_EDIT_LEGEND_ISOLATED_SET, + legendIsolated: false, + } + ) + + expect(result.legendIsolated).toBeUndefined() + }) + }) + + it('sets the legend set', () => { + const legendSet = { id: 'ls1' } + + const result = layerEdit( + {}, + { type: types.LAYER_EDIT_LEGEND_SET_SET, legendSet } + ) + + expect(result.legendSet).toBe(legendSet) + }) + + describe('LAYER_EDIT_NO_DATA_LEGEND_SET', () => { + it('sets noDataLegend when truthy', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_NO_DATA_LEGEND_SET, + payload: true, + } + ) + + expect(result.noDataLegend).toBe(true) + }) + + it('deletes noDataLegend when falsy', () => { + const result = layerEdit( + { noDataLegend: true }, + { + type: types.LAYER_EDIT_NO_DATA_LEGEND_SET, + payload: false, + } + ) + + expect(result.noDataLegend).toBeUndefined() + }) + }) + + describe('LAYER_EDIT_UNCLASSIFIED_LEGEND_SET', () => { + it('sets unclassifiedLegend when truthy', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_UNCLASSIFIED_LEGEND_SET, + payload: true, + } + ) + + expect(result.unclassifiedLegend).toBe(true) + }) + + it('deletes unclassifiedLegend when falsy', () => { + const result = layerEdit( + { unclassifiedLegend: true }, + { + type: types.LAYER_EDIT_UNCLASSIFIED_LEGEND_SET, + payload: false, + } + ) + + expect(result.unclassifiedLegend).toBeUndefined() + }) + }) + }) + + describe('event layer fields', () => { + describe('LAYER_EDIT_EVENT_STATUS_SET', () => { + it('deletes eventStatus when set to ALL', () => { + const result = layerEdit( + { eventStatus: 'ACTIVE' }, + { + type: types.LAYER_EDIT_EVENT_STATUS_SET, + status: EVENT_STATUS_ALL, + } + ) + + expect(result.eventStatus).toBeUndefined() + }) + + it('sets eventStatus for any other value', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_EVENT_STATUS_SET, + status: 'ACTIVE', + } + ) + + expect(result.eventStatus).toBe('ACTIVE') + }) + }) + + it('sets the event coordinate field and type', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_EVENT_COORDINATE_FIELD_SET, + fieldId: 'field1', + fieldType: 'COORDINATE', + } + ) + + expect(result.eventCoordinateField).toBe('field1') + expect(result.eventCoordinateFieldType).toBe('COORDINATE') + }) + + describe('LAYER_EDIT_FALLBACK_COORDINATE_FIELD_SET', () => { + it('deletes fallbackCoordinateField when set to NONE', () => { + const result = layerEdit( + { fallbackCoordinateField: 'field1' }, + { + type: types.LAYER_EDIT_FALLBACK_COORDINATE_FIELD_SET, + fieldId: NONE, + } + ) + + expect(result.fallbackCoordinateField).toBeUndefined() + }) + + it('sets fallbackCoordinateField for any other value', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_FALLBACK_COORDINATE_FIELD_SET, + fieldId: 'field1', + } + ) + + expect(result.fallbackCoordinateField).toBe('field1') + }) + }) + + it('sets eventClustering', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_EVENT_CLUSTERING_SET, + checked: true, + } + ) + + expect(result.eventClustering).toBe(true) + }) + + it('sets countFeaturesWithoutCoordinates', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_COUNT_FEATURES_WITHOUT_COORDS_SET, + checked: true, + } + ) + + expect(result.countFeaturesWithoutCoordinates).toBe(true) + }) + + it('sets countEventsOutsideOrgUnits', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_COUNT_EVENTS_OUTSIDE_OU_SET, + checked: true, + } + ) + + expect(result.countEventsOutsideOrgUnits).toBe(true) + }) + + it('sets eventPointRadius, parsed to an integer', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_EVENT_POINT_RADIUS_SET, + radius: '5', + } + ) + + expect(result.eventPointRadius).toBe(5) + }) + + it('sets eventPointColor', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_EVENT_POINT_COLOR_SET, + color: '#fff', + } + ) + + expect(result.eventPointColor).toBe('#fff') + }) + + it('sets relatedPointColor', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_RELATED_POINT_COLOR_SET, + color: '#fff', + } + ) + + expect(result.relatedPointColor).toBe('#fff') + }) + + it('sets relatedPointRadius, parsed to an integer', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_RELATED_POINT_RADIUS_SET, + radius: '8', + } + ) + + expect(result.relatedPointRadius).toBe(8) + }) + + it('sets relationshipLineColor', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_RELATIONSHIP_LINE_COLOR_SET, + color: '#fff', + } + ) + + expect(result.relationshipLineColor).toBe('#fff') + }) + + it('sets geometryCentroid', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_GEOMETRY_CENTROIDS_SET, + payload: true, + } + ) + + expect(result.geometryCentroid).toBe(true) + }) }) - it('keeps countEventsOutsideOrgUnits and countFeaturesWithoutCoordinates when the program stage changes', () => { - const state = { - countEventsOutsideOrgUnits: true, - countFeaturesWithoutCoordinates: true, - } + describe('org unit fields', () => { + it('sets organisationUnitGroupSet', () => { + const ougs = { id: 'ougs1' } + + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_ORGANISATION_UNIT_GROUP_SET, + organisationUnitGroupSet: ougs, + } + ) + + expect(result.organisationUnitGroupSet).toBe(ougs) + }) + + it('sets organisationUnitColor', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_ORGANISATION_UNIT_COLOR_SET, + color: '#fff', + } + ) - const result = layerEdit(state, { - type: types.LAYER_EDIT_PROGRAM_STAGE_SET, - programStage: { id: 'stage1' }, + expect(result.organisationUnitColor).toBe('#fff') }) - expect(result.countEventsOutsideOrgUnits).toBe(true) - expect(result.countFeaturesWithoutCoordinates).toBe(true) + describe('LAYER_EDIT_ORGANISATION_UNIT_FIELD_SET', () => { + it('sets orgUnitField/displayName and clears areaRadius for a real field', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_ORGANISATION_UNIT_FIELD_SET, + payload: { id: 'field1', name: 'Field 1' }, + } + ) + + expect(result.orgUnitField).toBe('field1') + expect(result.orgUnitFieldDisplayName).toBe('Field 1') + expect(result.areaRadius).toBe(null) + }) + + it('defaults areaRadius to EE_BUFFER and clears the display name when NONE', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_ORGANISATION_UNIT_FIELD_SET, + payload: { id: NONE, name: 'None' }, + } + ) + + expect(result.orgUnitField).toBe(NONE) + expect(result.orgUnitFieldDisplayName).toBe(null) + expect(result.areaRadius).toBe(EE_BUFFER) + }) + }) + + it('sets organisation units as the rows payload', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_ORGANISATION_UNITS_SET, + payload: { id: 'ou1' }, + } + ) + + expect(result.rows).toEqual([{ id: 'ou1' }]) + }) + + it('sets the organisation unit tree path', () => { + const rows = [{ dimension: 'ou', items: [{ id: 'ou1' }] }] + + const result = layerEdit( + { rows }, + { + type: types.LAYER_EDIT_ORGANISATION_UNIT_PATH_SET, + id: 'ou1', + path: '/root/ou1', + } + ) + + expect(result.rows).toEqual( + setOrgUnitPathInRows(rows, 'ou1', '/root/ou1') + ) + }) + + it('sets organisationUnitSelectionMode', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_ORGANISATION_UNIT_MODE_SET, + payload: 'SELECT', + } + ) + + expect(result.organisationUnitSelectionMode).toBe('SELECT') + }) + + it('sets bufferRadius (areaRadius)', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_BUFFER_RADIUS_SET, + radius: 500, + } + ) + + expect(result.areaRadius).toBe(500) + }) + + it('sets radiusLow, parsed to an integer', () => { + const result = layerEdit( + {}, + { type: types.LAYER_EDIT_RADIUS_LOW_SET, radius: '2' } + ) + + expect(result.radiusLow).toBe(2) + }) + + it('sets radiusHigh, parsed to an integer', () => { + const result = layerEdit( + {}, + { type: types.LAYER_EDIT_RADIUS_HIGH_SET, radius: '10' } + ) + + expect(result.radiusHigh).toBe(10) + }) + }) + + describe('labels', () => { + it('sets label visibility', () => { + const result = layerEdit( + {}, + { type: types.LAYER_EDIT_LABELS_SET, isChecked: true } + ) + + expect(result.labels).toBe(true) + }) + + it('sets labelTemplate', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_LABEL_TEMPLATE, + template: '{name}', + } + ) + + expect(result.labelTemplate).toBe('{name}') + }) + + it('sets labelFontColor', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_LABEL_FONT_COLOR_SET, + color: '#fff', + } + ) + + expect(result.labelFontColor).toBe('#fff') + }) + + it('sets labelFontSize', () => { + const result = layerEdit( + {}, + { type: types.LAYER_EDIT_LABEL_FONT_SIZE_SET, size: 12 } + ) + + expect(result.labelFontSize).toBe(12) + }) + + it('sets labelFontWeight', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_LABEL_FONT_WEIGHT_SET, + weight: 'bold', + } + ) + + expect(result.labelFontWeight).toBe('bold') + }) + + it('sets labelFontStyle', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_LABEL_FONT_STYLE_SET, + style: 'italic', + } + ) + + expect(result.labelFontStyle).toBe('italic') + }) + + describe('LAYER_EDIT_LABEL_DATA_ITEM_ID_SET', () => { + it('sets labelDataItem when an item is given', () => { + const item = { id: 'de1' } + + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_LABEL_DATA_ITEM_ID_SET, + item, + } + ) + + expect(result.labelDataItem).toBe(item) + }) + + it('deletes labelDataItem when no item is given', () => { + const result = layerEdit( + { labelDataItem: { id: 'de1' } }, + { + type: types.LAYER_EDIT_LABEL_DATA_ITEM_ID_SET, + item: null, + } + ) + + expect(result.labelDataItem).toBeUndefined() + }) + }) + }) + + describe('earth engine fields', () => { + it('sets the layer style, merging with any existing style', () => { + const result = layerEdit( + { style: { color: '#000' } }, + { + type: types.LAYER_EDIT_STYLE_SET, + payload: { weight: 2 }, + } + ) + + expect(result.style).toEqual({ color: '#000', weight: 2 }) + }) + + it('sets the collection filter', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_FILTER_SET, + filter: 'NDVI > 0.5', + } + ) + + expect(result.filter).toBe('NDVI > 0.5') + }) + + it('sets the band', () => { + const result = layerEdit( + {}, + { type: types.LAYER_EDIT_BAND_SET, payload: 'B4' } + ) + + expect(result.band).toBe('B4') + }) + + it('sets the earth engine period', () => { + const period = { id: 'period1' } + + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_EARTH_ENGINE_PERIOD_SET, + payload: period, + } + ) + + expect(result.period).toBe(period) + }) + }) + + describe('tracked entity fields', () => { + it('sets trackedEntityType and resets program/programStage/relationshipType/relationshipOutsideProgram', () => { + const state = { + program: { id: 'p1' }, + programStage: { id: 's1' }, + relationshipType: { id: 'r1' }, + relationshipOutsideProgram: true, + } + + const result = layerEdit(state, { + type: types.LAYER_EDIT_TRACKED_ENTITY_TYPE_SET, + trackedEntityType: { id: 'tet1' }, + }) + + expect(result).toEqual({ + trackedEntityType: { id: 'tet1' }, + program: null, + programStage: null, + relationshipType: null, + relationshipOutsideProgram: null, + }) + }) + + it('sets the tracked entity relationship type', () => { + const relationshipType = { id: 'rt1' } + + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_TRACKED_ENTITY_RELATIONSHIP_TYPE_SET, + relationshipType, + } + ) + + expect(result.relationshipType).toBe(relationshipType) + }) + + it('sets relationshipOutsideProgram', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_TRACKED_ENTITY_RELATIONSHIP_OUTSIDE_PROGRAM_SET, + payload: true, + } + ) + + expect(result.relationshipOutsideProgram).toBe(true) + }) + + describe('LAYER_EDIT_PROGRAM_STATUS_SET', () => { + it('deletes programStatus when set to ALL', () => { + const result = layerEdit( + { programStatus: 'ACTIVE' }, + { + type: types.LAYER_EDIT_PROGRAM_STATUS_SET, + payload: 'ALL', + } + ) + + expect(result.programStatus).toBeUndefined() + }) + + it('sets programStatus for any other value', () => { + const result = layerEdit( + {}, + { + type: types.LAYER_EDIT_PROGRAM_STATUS_SET, + payload: 'ACTIVE', + } + ) + + expect(result.programStatus).toBe('ACTIVE') + }) + }) + + it('sets followUp status', () => { + const result = layerEdit( + {}, + { type: types.LAYER_EDIT_FOLLOW_UP_SET, payload: true } + ) + + expect(result.followUp).toBe(true) + }) + + it('sets feature style, merging with any existing style', () => { + const result = layerEdit( + { featureStyle: { color: '#000' } }, + { + type: types.LAYER_EDIT_FEATURE_STYLE_SET, + payload: { weight: 2 }, + } + ) + + expect(result.featureStyle).toEqual({ color: '#000', weight: 2 }) + }) + }) + + it('returns the current state for unknown actions', () => { + const state = { id: 'layer1' } + + expect(layerEdit(state, { type: 'UNKNOWN' })).toBe(state) }) }) diff --git a/src/reducers/__tests__/layerSources.spec.js b/src/reducers/__tests__/layerSources.spec.js new file mode 100644 index 000000000..8247ee87d --- /dev/null +++ b/src/reducers/__tests__/layerSources.spec.js @@ -0,0 +1,40 @@ +import * as types from '../../constants/actionTypes.js' +import layerSources from '../layerSources.js' + +describe('layerSources reducer', () => { + it('returns an empty array by default', () => { + expect(layerSources(undefined, {})).toEqual([]) + }) + + it('initializes the layer sources', () => { + const payload = ['src1', 'src2'] + + expect( + layerSources([], { type: types.LAYER_SOURCES_INIT, payload }) + ).toBe(payload) + }) + + it('adds a layer source', () => { + expect( + layerSources(['src1'], { + type: types.LAYER_SOURCE_ADD, + payload: 'src2', + }) + ).toEqual(['src1', 'src2']) + }) + + it('removes a layer source', () => { + expect( + layerSources(['src1', 'src2'], { + type: types.LAYER_SOURCE_REMOVE, + payload: 'src1', + }) + ).toEqual(['src2']) + }) + + it('returns the current state for unknown actions', () => { + const state = ['src1'] + + expect(layerSources(state, { type: 'UNKNOWN' })).toBe(state) + }) +}) diff --git a/src/reducers/__tests__/map.spec.js b/src/reducers/__tests__/map.spec.js index 26a7e6674..1c70810ea 100644 --- a/src/reducers/__tests__/map.spec.js +++ b/src/reducers/__tests__/map.spec.js @@ -1,5 +1,6 @@ import * as types from '../../constants/actionTypes.js' -import map from '../map.js' +import { isValidUid } from '../../util/uid.js' +import map, { defaultBasemapState } from '../map.js' // The Layers panel renders overlay layers in the REVERSE of `mapViews` order // (top of panel = last map view), so LAYER_SORT translates an oldIndex/newIndex @@ -7,7 +8,7 @@ import map from '../map.js' describe('map reducer - LAYER_SORT', () => { const layer = (id) => ({ id }) - // stored order [A, B, C] -> displayed (reversed) order [C, B, A] + // Stored order [A, B, C] -> displayed (reversed) order [C, B, A] const stateWith = (...ids) => ({ mapViews: ids.map(layer), }) @@ -16,7 +17,7 @@ describe('map reducer - LAYER_SORT', () => { [...state.mapViews].reverse().map((mv) => mv.id) it('moving the top layer to the bottom (display space) reorders mapViews', () => { - // displayed: [C, B, A] -> drag C (index 0) down to index 2 -> [B, A, C] + // Displayed: [C, B, A] -> drag C (index 0) down to index 2 -> [B, A, C] const state = stateWith('A', 'B', 'C') const result = map(state, { @@ -30,7 +31,7 @@ describe('map reducer - LAYER_SORT', () => { }) it('moving the bottom layer to the top (display space) reorders mapViews', () => { - // displayed: [C, B, A] -> drag A (index 2) up to index 0 -> [A, C, B] + // Displayed: [C, B, A] -> drag A (index 2) up to index 0 -> [A, C, B] const state = stateWith('A', 'B', 'C') const result = map(state, { @@ -44,7 +45,7 @@ describe('map reducer - LAYER_SORT', () => { }) it('swapping two adjacent layers in display space swaps them in mapViews', () => { - // displayed: [C, B, A] -> drag B (index 1) up to index 0 -> [B, C, A] + // Displayed: [C, B, A] -> drag B (index 1) up to index 0 -> [B, C, A] const state = stateWith('A', 'B', 'C') const result = map(state, { @@ -79,3 +80,503 @@ describe('map reducer - LAYER_SORT', () => { expect(state.mapViews.map((mv) => mv.id)).toEqual(['A', 'B', 'C']) }) }) + +const defaultState = { + bounds: [ + [-18.7, -34.9], + [50.2, 35.9], + ], + basemap: defaultBasemapState, + mapViews: [], +} + +describe('map reducer - MAP_NEW / MAP_SET', () => { + it('resets to the default state when starting a new map', () => { + const state = { + bounds: [ + [0, 0], + [1, 1], + ], + basemap: { id: 'osm' }, + mapViews: [{ id: 'layer1' }], + } + + expect(map(state, { type: types.MAP_NEW })).toEqual(defaultState) + }) + + it('merges the payload into the default state when setting the map', () => { + const result = map(defaultState, { + type: types.MAP_SET, + payload: { + bounds: [ + [0, 0], + [1, 1], + ], + mapViews: [{ id: 'layer1' }], + basemap: { opacity: 0.5 }, + }, + }) + + expect(result).toEqual({ + bounds: [ + [0, 0], + [1, 1], + ], + mapViews: [{ id: 'layer1' }], + basemap: { ...defaultBasemapState, opacity: 0.5 }, + }) + }) +}) + +describe('map reducer - MAP_PROPS_SET / MAP_COORDINATE_OPEN / MAP_COORDINATE_CLOSE', () => { + it('merges props into the state', () => { + const result = map(defaultState, { + type: types.MAP_PROPS_SET, + payload: { zoom: 5, center: [1, 2] }, + }) + + expect(result.zoom).toBe(5) + expect(result.center).toEqual([1, 2]) + }) + + it('opens the coordinate popup', () => { + const result = map(defaultState, { + type: types.MAP_COORDINATE_OPEN, + payload: [1, 2], + }) + + expect(result.coordinatePopup).toEqual([1, 2]) + }) + + it('closes the coordinate popup', () => { + const state = { ...defaultState, coordinatePopup: [1, 2] } + + const result = map(state, { type: types.MAP_COORDINATE_CLOSE }) + + expect(result.coordinatePopup).toBe(null) + }) +}) + +describe('map reducer - basemap actions', () => { + it('replaces the basemap when selecting a different one', () => { + const state = { + ...defaultState, + basemap: { id: 'osm', isVisible: true, isExpanded: true }, + } + + const result = map(state, { + type: types.BASEMAP_SELECTED, + payload: { id: 'satellite' }, + }) + + expect(result.basemap).toEqual({ + id: 'satellite', + isVisible: true, + isExpanded: true, + }) + }) + + it('is a no-op when selecting the already-active basemap', () => { + const basemap = { id: 'osm', isVisible: true, isExpanded: true } + const state = { ...defaultState, basemap } + + const result = map(state, { + type: types.BASEMAP_SELECTED, + payload: { id: 'osm' }, + }) + + expect(result.basemap).toBe(basemap) + }) + + it('sets the basemap opacity', () => { + const state = { ...defaultState, basemap: { opacity: 1 } } + + const result = map(state, { + type: types.BASEMAP_CHANGE_OPACITY, + opacity: 0.5, + }) + + expect(result.basemap.opacity).toBe(0.5) + }) + + it('toggles basemap expand', () => { + const state = { ...defaultState, basemap: { isExpanded: true } } + + const result = map(state, { type: types.BASEMAP_TOGGLE_EXPAND }) + + expect(result.basemap.isExpanded).toBe(false) + }) + + it('toggles basemap visibility', () => { + const state = { ...defaultState, basemap: { isVisible: true } } + + const result = map(state, { type: types.BASEMAP_TOGGLE_VISIBILITY }) + + expect(result.basemap.isVisible).toBe(false) + }) +}) + +describe('map reducer - LAYER_ADD', () => { + it('adds a new layer with a generated id and defaults isVisible to true', () => { + const result = map( + { ...defaultState, mapViews: [] }, + { + type: types.LAYER_ADD, + payload: { type: 'thematic', name: 'Layer 1' }, + } + ) + + expect(result.mapViews).toHaveLength(1) + expect(isValidUid(result.mapViews[0].id)).toBe(true) + expect(result.mapViews[0].name).toBe('Layer 1') + expect(result.mapViews[0].isVisible).toBe(true) + }) + + it('respects an explicit isVisible value', () => { + const result = map( + { ...defaultState, mapViews: [] }, + { + type: types.LAYER_ADD, + payload: { id: 'ext1', type: 'external', isVisible: false }, + } + ) + + expect(result.mapViews[0].isVisible).toBe(false) + }) + + it('does not re-add a layer whose id already exists (external layers can only be added once)', () => { + const existing = { id: 'ext1', type: 'external' } + const state = { ...defaultState, mapViews: [existing] } + + const result = map(state, { + type: types.LAYER_ADD, + payload: { id: 'ext1', type: 'external' }, + }) + + expect(result).toBe(state) + }) +}) + +describe('map reducer - LAYER_REMOVE / LAYER_DUPLICATE', () => { + it('removes a layer by id', () => { + const state = { + ...defaultState, + mapViews: [{ id: 'layer1' }, { id: 'layer2' }], + } + + const result = map(state, { type: types.LAYER_REMOVE, id: 'layer1' }) + + expect(result.mapViews).toEqual([{ id: 'layer2' }]) + }) + + it('returns state unchanged when duplicating a layer that is not found', () => { + const state = { ...defaultState, mapViews: [{ id: 'layer1' }] } + + const result = map(state, { + type: types.LAYER_DUPLICATE, + id: 'missing', + }) + + expect(result).toBe(state) + }) + + it('inserts a duplicate right after the source, with a new id and no isLoading/coordinate', () => { + const state = { + ...defaultState, + mapViews: [ + { + id: 'layer1', + name: 'Layer 1', + isLoading: true, + coordinate: [1, 2], + }, + { id: 'layer2', name: 'Layer 2' }, + ], + } + + const result = map(state, { + type: types.LAYER_DUPLICATE, + id: 'layer1', + }) + + expect(result.mapViews).toHaveLength(3) + expect(result.mapViews[0]).toBe(state.mapViews[0]) + + const duplicate = result.mapViews[1] + expect(duplicate.name).toBe('Layer 1') + expect(duplicate.id).not.toBe('layer1') + expect(isValidUid(duplicate.id)).toBe(true) + expect(duplicate.isLoading).toBeUndefined() + expect(duplicate.coordinate).toBeUndefined() + + expect(result.mapViews[2]).toBe(state.mapViews[1]) + }) +}) + +describe('map reducer - per-layer delegation', () => { + describe('LAYER_UPDATE', () => { + it('replaces the matching layer and leaves others untouched', () => { + const other = { id: 'layer2', name: 'Other' } + const state = { + ...defaultState, + mapViews: [{ id: 'layer1', name: 'Old' }, other], + } + + const result = map(state, { + type: types.LAYER_UPDATE, + payload: { id: 'layer1', name: 'New', opacity: 0.5 }, + }) + + expect(result.mapViews[0]).toEqual({ + id: 'layer1', + name: 'New', + opacity: 0.5, + }) + expect(result.mapViews[1]).toBe(other) + }) + }) + + describe('LAYER_EDIT', () => { + it('does not modify any mapView (the layer sub-reducer has no matching case)', () => { + const layer1 = { id: 'layer1', name: 'Original' } + const state = { ...defaultState, mapViews: [layer1] } + + const result = map(state, { + type: types.LAYER_EDIT, + payload: { id: 'layer1', name: 'Changed' }, + }) + + expect(result.mapViews[0]).toBe(layer1) + expect(result.mapViews).not.toBe(state.mapViews) + }) + }) + + describe('LAYER_CHANGE_OPACITY', () => { + it('updates the matching layer only', () => { + const other = { id: 'layer2', opacity: 1 } + const state = { + ...defaultState, + mapViews: [{ id: 'layer1', opacity: 1 }, other], + } + + const result = map(state, { + type: types.LAYER_CHANGE_OPACITY, + id: 'layer1', + opacity: 0.3, + }) + + expect(result.mapViews[0].opacity).toBe(0.3) + expect(result.mapViews[1]).toBe(other) + }) + }) + + describe('LAYER_LOADING_SET', () => { + it('sets isLoading on the matching layer only', () => { + const other = { id: 'layer2' } + const state = { + ...defaultState, + mapViews: [{ id: 'layer1' }, other], + } + + const result = map(state, { + type: types.LAYER_LOADING_SET, + id: 'layer1', + }) + + expect(result.mapViews[0].isLoading).toBe(true) + expect(result.mapViews[1]).toBe(other) + }) + }) + + describe('LAYER_TOGGLE_VISIBILITY', () => { + it('toggles isVisible on the matching layer only', () => { + const other = { id: 'layer2', isVisible: true } + const state = { + ...defaultState, + mapViews: [{ id: 'layer1', isVisible: true }, other], + } + + const result = map(state, { + type: types.LAYER_TOGGLE_VISIBILITY, + id: 'layer1', + }) + + expect(result.mapViews[0].isVisible).toBe(false) + expect(result.mapViews[1]).toBe(other) + }) + }) + + describe('LAYER_TOGGLE_EXPAND', () => { + it('toggles isExpanded on the matching layer only', () => { + const other = { id: 'layer2', isExpanded: true } + const state = { + ...defaultState, + mapViews: [{ id: 'layer1', isExpanded: true }, other], + } + + const result = map(state, { + type: types.LAYER_TOGGLE_EXPAND, + id: 'layer1', + }) + + expect(result.mapViews[0].isExpanded).toBe(false) + expect(result.mapViews[1]).toBe(other) + }) + }) + + describe('DATA_FILTER_SET', () => { + it('sets a data filter field on the matching layer only', () => { + const other = { id: 'layer2' } + const state = { + ...defaultState, + mapViews: [{ id: 'layer1' }, other], + } + + const result = map(state, { + type: types.DATA_FILTER_SET, + layerId: 'layer1', + fieldId: 'field1', + filter: { operator: 'eq', value: 1 }, + }) + + expect(result.mapViews[0].dataFilters).toEqual({ + field1: { operator: 'eq', value: 1 }, + }) + expect(result.mapViews[1]).toBe(other) + }) + }) + + describe('DATA_FILTER_CLEAR', () => { + it('removes a data filter field on the matching layer only', () => { + const other = { id: 'layer2' } + const state = { + ...defaultState, + mapViews: [ + { + id: 'layer1', + dataFilters: { field1: {}, field2: {} }, + }, + other, + ], + } + + const result = map(state, { + type: types.DATA_FILTER_CLEAR, + layerId: 'layer1', + fieldId: 'field1', + }) + + expect(result.mapViews[0].dataFilters).toEqual({ field2: {} }) + expect(result.mapViews[1]).toBe(other) + }) + }) + + describe('MAP_EARTH_ENGINE_VALUE_SHOW', () => { + it('sets the coordinate on the matching layer only', () => { + const other = { id: 'layer2' } + const state = { + ...defaultState, + mapViews: [{ id: 'layer1' }, other], + } + + const result = map(state, { + type: types.MAP_EARTH_ENGINE_VALUE_SHOW, + layerId: 'layer1', + coordinate: [1, 2], + }) + + expect(result.mapViews[0].coordinate).toEqual([1, 2]) + expect(result.mapViews[1]).toBe(other) + }) + }) +}) + +describe('map reducer - MAP_ALERTS_CLEAR', () => { + it('clears top-level alerts and per-layer alerts', () => { + const state = { + ...defaultState, + alerts: [{ message: 'Oops' }], + mapViews: [{ id: 'layer1', alerts: [{ message: 'Layer error' }] }], + } + + const result = map(state, { type: types.MAP_ALERTS_CLEAR }) + + expect(result.alerts).toBeUndefined() + expect(result.mapViews[0].alerts).toBeUndefined() + }) +}) + +describe('map reducer - MAP_PERIODS_SYNC', () => { + it('adds a new pe filter for map views matching the rendering strategy', () => { + const state = { + ...defaultState, + mapViews: [ + { + id: 'layer1', + renderingStrategy: 'SINGLE', + filters: [{ dimension: 'dx', items: [] }], + }, + ], + } + const periods = [{ id: '2021' }] + + const result = map(state, { + type: types.MAP_PERIODS_SYNC, + renderingStrategy: 'SINGLE', + periods, + }) + + expect(result.mapViews[0].filters).toEqual([ + { dimension: 'dx', items: [] }, + { dimension: 'pe', items: periods }, + ]) + expect(result.mapViews[0].isLoaded).toBe(false) + }) + + it('updates an existing pe filter in place', () => { + const state = { + ...defaultState, + mapViews: [ + { + id: 'layer1', + renderingStrategy: 'SINGLE', + filters: [{ dimension: 'pe', items: [{ id: '2020' }] }], + }, + ], + } + const periods = [{ id: '2021' }] + + const result = map(state, { + type: types.MAP_PERIODS_SYNC, + renderingStrategy: 'SINGLE', + periods, + }) + + expect(result.mapViews[0].filters).toEqual([ + { dimension: 'pe', items: periods }, + ]) + }) + + it('leaves map views with a different rendering strategy untouched', () => { + const other = { id: 'layer1', renderingStrategy: 'TIMELINE' } + const state = { ...defaultState, mapViews: [other] } + + const result = map(state, { + type: types.MAP_PERIODS_SYNC, + renderingStrategy: 'SINGLE', + periods: [{ id: '2021' }], + }) + + expect(result.mapViews[0]).toBe(other) + }) +}) + +describe('map reducer - default', () => { + it('returns the default state by default', () => { + expect(map(undefined, { type: 'UNKNOWN' })).toEqual(defaultState) + }) + + it('returns the current state for unknown actions', () => { + expect(map(defaultState, { type: 'UNKNOWN' })).toBe(defaultState) + }) +}) diff --git a/src/reducers/__tests__/orgUnitProfile.spec.js b/src/reducers/__tests__/orgUnitProfile.spec.js new file mode 100644 index 000000000..897670962 --- /dev/null +++ b/src/reducers/__tests__/orgUnitProfile.spec.js @@ -0,0 +1,34 @@ +import * as types from '../../constants/actionTypes.js' +import orgUnitProfile from '../orgUnitProfile.js' + +describe('orgUnitProfile reducer', () => { + it('returns null by default', () => { + expect(orgUnitProfile(undefined, {})).toBe(null) + }) + + it('sets the org unit profile', () => { + const payload = 'ou1' + + expect( + orgUnitProfile(null, { + type: types.ORGANISATION_UNIT_PROFILE_SET, + payload, + }) + ).toBe(payload) + }) + + it.each([ + types.ORGANISATION_UNIT_PROFILE_CLOSE, + types.FEATURE_PROFILE_SET, + types.INTERPRETATIONS_PANEL_OPEN, + types.DOWNLOAD_MODE_CLOSE, + types.MAP_NEW, + types.MAP_SET, + ])('clears the org unit profile on %s', (type) => { + expect(orgUnitProfile('ou1', { type })).toBe(null) + }) + + it('returns the current state for unknown actions', () => { + expect(orgUnitProfile('ou1', { type: 'UNKNOWN' })).toBe('ou1') + }) +}) diff --git a/src/reducers/map.js b/src/reducers/map.js index fe355ed6f..7794693f0 100644 --- a/src/reducers/map.js +++ b/src/reducers/map.js @@ -73,6 +73,7 @@ const basemap = (state, action) => { isVisible: !state.isVisible, } + // TODO: unreachable default: return state }