diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/flood-level-lines.js b/assets/src/scripts/monitoring-location/components/hydrograph/flood-level-lines.js index c564d80f4595fbecbac0bcc9b547e8490fa20cb7..34600fca4d42018d41d72eafe3a3afcff7ca906a 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/flood-level-lines.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/flood-level-lines.js @@ -26,7 +26,7 @@ export const drawFloodLevelLines = function(svg, {visible, xscale, yscale, flood const yRange = yscale(level.value); const floodLine = d3Line()([[xRange[0], yRange], [xRange[1], yRange]]); group.append('path') - .classed('waterwatch-data-series', true) + .classed('flood-levels-series', true) .classed(level.class, true) .attr('d', floodLine); } diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/index.js b/assets/src/scripts/monitoring-location/components/hydrograph/index.js index 43d7a814e3beffad88a8f6e94130e4f48b711ad4..c8573079cd121a0a24c2bbe3436cf366e9baea6e 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/index.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/index.js @@ -16,7 +16,7 @@ import {setSelectedParameterCode, setCompareDataVisibility, setSelectedTimeSpan, setSelectedIVMethodID } from 'ml/store/hydrograph-state'; -import {Actions as floodDataActions} from 'ml/store/flood-inundation'; +import {Actions as floodDataActions} from 'ml/store/flood-data'; import {getPreferredIVMethodID} from './selectors/time-series-data'; @@ -102,9 +102,9 @@ export const attachToNode = function(store, } let fetchDataPromises = [fetchHydrographDataPromise]; - // Fetch waterwatch flood levels + // Fetch flood levels if (config.ivPeriodOfRecord && config.GAGE_HEIGHT_PARAMETER_CODE in config.ivPeriodOfRecord) { - const fetchFloodLevelsPromise = store.dispatch(floodDataActions.retrieveWaterwatchData(siteno)); + const fetchFloodLevelsPromise = store.dispatch(floodDataActions.retrieveFloodLevels(siteno)); // If flood levels are to be shown then wait to render the hydrograph until those have been fetched. if (parameterCode === config.GAGE_HEIGHT_PARAMETER_CODE) { fetchDataPromises.push(fetchFloodLevelsPromise); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/index.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/index.test.js index 18fbdd165ef993a2a4cc463f2ea29fde9f25f5d1..a9ebb5bb8c018b7a7bb231823c6c51631207451f 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/index.test.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/index.test.js @@ -1,4 +1,6 @@ import {select, selectAll} from 'd3-selection'; +import {enableFetchMocks, disableFetchMocks} from 'jest-fetch-mock'; +import mockConsole from 'jest-mock-console'; import sinon from 'sinon'; import * as utils from 'ui/utils'; @@ -6,7 +8,7 @@ import config from 'ui/config'; import {configureStore} from 'ml/store'; -import {Actions as floodDataActions} from 'ml/store/flood-inundation'; +import {Actions as floodDataActions} from 'ml/store/flood-data'; import * as hydrographData from 'ml/store/hydrograph-data'; import * as hydrographParameters from 'ml/store/hydrograph-parameters'; @@ -63,6 +65,18 @@ describe('monitoring-location/components/hydrograph module', () => { parameterCode: '72019' }; + let restoreConsole; + + beforeAll(() => { + enableFetchMocks(); + restoreConsole = mockConsole(); + }); + + afterAll(() => { + disableFetchMocks(); + restoreConsole(); + }); + beforeEach(() => { let body = select('body'); body.append('a') @@ -94,7 +108,7 @@ describe('monitoring-location/components/hydrograph module', () => { describe('Tests for initial data fetching and setting', () => { let store; - let retrieveHydrographDataSpy, retrieveHydrographParametersSpy, retrieveWaterwatchDataSpy; + let retrieveHydrographDataSpy, retrieveHydrographParametersSpy, retrieveFloodLevelsSpy; beforeEach(() => { store = configureStore({ @@ -108,7 +122,7 @@ describe('monitoring-location/components/hydrograph module', () => { }); retrieveHydrographDataSpy = jest.spyOn(hydrographData, 'retrieveHydrographData'); retrieveHydrographParametersSpy = jest.spyOn(hydrographParameters, 'retrieveHydrographParameters'); - retrieveWaterwatchDataSpy = jest.spyOn(floodDataActions, 'retrieveWaterwatchData'); + retrieveFloodLevelsSpy = jest.spyOn(floodDataActions, 'retrieveFloodLevels'); }); it('Loading indicator should be shown', () => { @@ -209,9 +223,9 @@ describe('monitoring-location/components/hydrograph module', () => { expect(retrieveHydrographParametersSpy).toHaveBeenCalledWith('11112222'); }); - it('Should fetch the waterwatch flood levels', () => { + it('Should fetch the flood levels', () => { attachToNode(store, graphNode, INITIAL_PARAMETERS); - expect(retrieveWaterwatchDataSpy).toHaveBeenCalledWith('11112222'); + expect(retrieveFloodLevelsSpy).toHaveBeenCalledWith('11112222'); }); it('Should fetch the data and set the hydrograph state but not does not fetch hydrograph parameters when showOnlyGraph is true', () => { @@ -233,7 +247,7 @@ describe('monitoring-location/components/hydrograph module', () => { selectedTimeSpan: 'P7D', showCompareIVData: false }); - expect(retrieveWaterwatchDataSpy).toHaveBeenCalled(); + expect(retrieveFloodLevelsSpy).toHaveBeenCalled(); expect(retrieveHydrographParametersSpy).not.toHaveBeenCalled(); }); }); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/flood-level-data.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/flood-level-data.js index 429eafd02562b4606796d541acb558e641760254..de698713c3ef609e34227ee108137d164f4c670e 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/flood-level-data.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/flood-level-data.js @@ -1,21 +1,21 @@ import {createSelector} from 'reselect'; -import {getWaterwatchFloodLevels} from 'ml/selectors/flood-data-selector'; +import {getFloodLevels} from 'ml/selectors/flood-data-selector'; const STAGES = { - actionStage: { + action_stage: { label: 'Action stage', class: 'action-stage' }, - floodStage: { + flood_stage: { label: 'Minor flood stage', class: 'minor-flood-stage' }, - moderateFloodStage: { + moderate_flood_stage: { label: 'Moderate flood stage', class: 'moderate-flood-stage' }, - majorFloodStage: { + major_flood_stage: { label: 'Major flood stage', class: 'major-flood-stage' } @@ -25,20 +25,21 @@ const STAGES = { * Returns a selector function which returns an array of {Object}. Each object * represents a flood level and includes the properties: * @prop {Number} value - the value of the flood level + * @prop {String} displayValue - the string value (preserves significant digits) * @prop {String} label - a human friendly label for the flood level * @prop {String} class - a class to be used to decorate the flood level */ export const getFloodLevelData = createSelector( - getWaterwatchFloodLevels, + getFloodLevels, levels => { const result = []; if (levels) { - Object.keys(levels).forEach(stage => { + Object.keys(STAGES).forEach(stage => { if (levels[stage]) { result.push({ - value: levels[stage], - label: STAGES[stage].label, - class: STAGES[stage].class + ...STAGES[stage], + displayValue: levels[stage], + value: parseFloat(levels[stage]) }); } }); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/flood-level-data.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/flood-level-data.test.js index 413a5c696c8449cb03f25e6bb4dafd3289ed4183..1f4617fe000d5ceeb1a6c3875bd7bb10caeedee1 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/flood-level-data.test.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/flood-level-data.test.js @@ -25,6 +25,7 @@ describe('monitoring-location/components/hydrograph/selectors/flood-level-data', expect(result).toHaveLength(4); expect(result[0].label).toBeDefined(); expect(result[0].class).toBeDefined(); + expect(result.map(level => level.displayValue)).toEqual(['5', '10', '12', '14']); expect(result.map(level => level.value)).toEqual([5, 10, 12, 14]); }); }); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/legend-data.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/legend-data.js index 69e89e5138ee1cf2a1c2551b20ac956d690eecac..abf96b600e0fb263e5e8ba143e3fee4553fd9cb9 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/legend-data.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/legend-data.js @@ -4,7 +4,7 @@ import config from 'ui/config'; import {defineLineMarker, defineRectangleMarker, defineCircleMarker, defineTextOnlyMarker} from 'd3render/markers'; -import {isWaterwatchVisible} from 'ml/selectors/flood-data-selector'; +import {showFloodLevels} from 'ml/selectors/flood-data-selector'; import {getPrimaryMedianStatisticsData, getPrimaryParameter} from 'ml/selectors/hydrograph-data-selector'; import {isCompareIVDataVisible, isMedianDataVisible} from 'ml/selectors/hydrograph-state-selector'; @@ -36,11 +36,11 @@ const getLegendDisplay = createSelector( getPrimaryMedianStatisticsData, getIVUniqueDataKinds('primary'), getIVUniqueDataKinds('compare'), - isWaterwatchVisible, + showFloodLevels, getFloodLevelData, getUniqueGWKinds, getPrimaryParameter, - (showCompare, showMedian, thresholds, medianSeries, currentClasses, compareClasses, showWaterWatch, floodLevels, gwLevelKinds, + (showCompare, showMedian, thresholds, medianSeries, currentClasses, compareClasses, showFloodLevels, floodLevels, gwLevelKinds, primaryParameter) => { const parameterCode = primaryParameter ? primaryParameter.parameterCode : null; const hasIVData = config.ivPeriodOfRecord && parameterCode ? parameterCode in config.ivPeriodOfRecord : false; @@ -49,7 +49,7 @@ const getLegendDisplay = createSelector( primaryIV: hasIVData ? currentClasses : undefined, compareIV: hasIVData && showCompare ? compareClasses : undefined, median: showMedian ? medianSeries : undefined, - floodLevels: showWaterWatch ? floodLevels : undefined, + floodLevels: showFloodLevels ? floodLevels : undefined, groundwaterLevels: hasGWLevelsData ? gwLevelKinds : undefined, thresholds: thresholds }; @@ -123,8 +123,8 @@ const getFloodLevelMarkers = function(floodLevels) { defineTextOnlyMarker(level.label), defineLineMarker( null, - `waterwatch-data-series ${level.class}`, - `${level.value} ft`) + `flood-levels-series ${level.class}`, + `${level.displayValue} ft`) ]; }); }; diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/time-series-graph.js b/assets/src/scripts/monitoring-location/components/hydrograph/time-series-graph.js index 9c9aca1c4f661efea74a5637e42b1272a27ae76c..8bcb0586c90968af2197a79ac80b9708fd13626f 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/time-series-graph.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/time-series-graph.js @@ -10,7 +10,7 @@ import {appendAxes} from 'd3render/axes'; import {renderMaskDefs} from 'd3render/data-masks'; import {appendInfoTooltip} from 'd3render/info-tooltip'; -import {isWaterwatchVisible} from 'ml/selectors/flood-data-selector'; +import {showFloodLevels} from 'ml/selectors/flood-data-selector'; import {getPrimaryParameter, getPrimaryMedianStatisticsData} from 'ml/selectors/hydrograph-data-selector'; import {getAxes} from './selectors/axes'; @@ -230,7 +230,7 @@ export const drawTimeSeriesGraphData = function(elem, store, showTooltip) { enableClip: () => true }))) .call(link(store, drawFloodLevelLines, createStructuredSelector({ - visible: isWaterwatchVisible, + visible: showFloodLevels, xscale: getMainXScale('current'), yscale: getMainYScale, floodLevels: getFloodLevelData diff --git a/assets/src/scripts/monitoring-location/components/map/flood-slider.js b/assets/src/scripts/monitoring-location/components/map/flood-slider.js index e7d84f0c1c21701a5924e7dfb316dc74294f851e..fc68eff28551bbd91d21f2519f0290a4feb9b26e 100644 --- a/assets/src/scripts/monitoring-location/components/map/flood-slider.js +++ b/assets/src/scripts/monitoring-location/components/map/flood-slider.js @@ -5,7 +5,7 @@ import {link} from 'ui/lib/d3-redux'; import {appendInfoTooltip} from 'd3render/info-tooltip'; import {getFloodStages, getFloodStageHeight, getFloodGageHeightStageIndex, hasFloodData} from 'ml/selectors/flood-data-selector'; -import {Actions} from 'ml/store/flood-inundation'; +import {Actions} from 'ml/store/flood-data'; const createSlider = function(elem, stages, store) { diff --git a/assets/src/scripts/monitoring-location/components/map/index.js b/assets/src/scripts/monitoring-location/components/map/index.js index e1e97be16d63639ead3507300ce3e74cefb31132..00bbab0a31b8d58bdf3cb1e1658027d7866e5e71 100644 --- a/assets/src/scripts/monitoring-location/components/map/index.js +++ b/assets/src/scripts/monitoring-location/components/map/index.js @@ -14,7 +14,7 @@ import {hasFloodData, getFloodExtent, getFloodStageHeight} from 'ml/selectors/fl import {hasNldiData, getNldiDownstreamFlows, getNldiUpstreamFlows, getNldiUpstreamBasin} from 'ml/selectors/nldi-data-selector'; import {Actions as nldiDataActions} from 'ml/store/nldi-data'; -import {Actions as floodInundationActions} from 'ml/store/flood-inundation'; +import {Actions as floodDataActions} from 'ml/store/flood-data'; import {floodSlider} from './flood-slider'; import {drawCircleMarkerLegend, drawFIMLegend, drawMonitoringLocationMarkerLegend} from './legend'; @@ -230,7 +230,7 @@ const siteMap = function(node, {siteno, latitude, longitude, zoom}, store) { * @param {Number} zoom - zoom level to initially set the map to */ export const attachToNode = function(store, node, {siteno, latitude, longitude, zoom}) { - store.dispatch(floodInundationActions.retrieveFloodData(siteno)); + store.dispatch(floodDataActions.retrieveFIMFloodData(siteno)); // hydrates the store with nldi data store.dispatch(nldiDataActions.retrieveNldiData(siteno)); diff --git a/assets/src/scripts/monitoring-location/components/map/index.test.js b/assets/src/scripts/monitoring-location/components/map/index.test.js index 5cb364e853e9cca454cf6838603c58d19aedcc40..1da05b368bea6bddb5366f84acda2271bfa240f9 100644 --- a/assets/src/scripts/monitoring-location/components/map/index.test.js +++ b/assets/src/scripts/monitoring-location/components/map/index.test.js @@ -1,4 +1,6 @@ import {select} from 'd3-selection'; +import {enableFetchMocks, disableFetchMocks} from 'jest-fetch-mock'; +import mockConsole from 'jest-mock-console'; import sinon from 'sinon'; import config from 'ui/config'; @@ -22,6 +24,18 @@ describe('monitoring-location/components/map module', () => { let mapNode; let store; let fakeServer; + let restoreConsole; + + beforeAll(() => { + enableFetchMocks(); + restoreConsole = mockConsole(); + }); + + afterAll(() => { + disableFetchMocks(); + restoreConsole(); + }); + beforeEach(() => { fakeServer = sinon.createFakeServer(); diff --git a/assets/src/scripts/monitoring-location/selectors/flood-data-selector.js b/assets/src/scripts/monitoring-location/selectors/flood-data-selector.js index a390def4a3938b7b11a7f392668d110dcca3bcc1..368d3b54f407c1433e9951b869d7c8d3d407daef 100644 --- a/assets/src/scripts/monitoring-location/selectors/flood-data-selector.js +++ b/assets/src/scripts/monitoring-location/selectors/flood-data-selector.js @@ -23,17 +23,16 @@ export const hasFloodData = createSelector( /* * Provides a function which returns True if waterwatch flood levels is not empty. */ -export const hasWaterwatchData = createSelector( +export const hasFloodLevels = createSelector( getFloodLevels, - (floodLevels) => - floodLevels != null + (floodLevels) => floodLevels != null ); /* - * Provides a function which returns True if waterwatch flood levels should be visible. + * Provides a function which returns True if flood levels should be visible. */ -export const isWaterwatchVisible = createSelector( - hasWaterwatchData, +export const showFloodLevels = createSelector( + hasFloodLevels, getPrimaryParameter, (hasFloodLevels, parameter) => hasFloodLevels && parameter && parameter.parameterCode === config.GAGE_HEIGHT_PARAMETER_CODE ); @@ -79,7 +78,7 @@ export const getFloodGageHeightStageIndex= createSelector( /* * Provides a function which returns the Waterwatch Flood Levels */ -export const getWaterwatchFloodLevels = createSelector( +export const getFloodLevelValues = createSelector( getFloodLevels, (floodLevels) => { return floodLevels ? { diff --git a/assets/src/scripts/monitoring-location/selectors/flood-data-selector.test.js b/assets/src/scripts/monitoring-location/selectors/flood-data-selector.test.js index cf7406aabe34c2e30f0e7fd84e6eda36bf09d629..97820072c7c3223d2021f745307268596a20c73d 100644 --- a/assets/src/scripts/monitoring-location/selectors/flood-data-selector.test.js +++ b/assets/src/scripts/monitoring-location/selectors/flood-data-selector.test.js @@ -1,6 +1,6 @@ import { getFloodStageHeight, hasFloodData, getFloodGageHeightStageIndex, - hasWaterwatchData, getWaterwatchFloodLevels, isWaterwatchVisible + showFloodLevels, getFloodLevels, hasFloodLevels } from './flood-data-selector'; describe('monitoring-location/selectors/flood-data-selector', () => { @@ -81,17 +81,17 @@ describe('monitoring-location/selectors/flood-data-selector', () => { }); }); - describe('hasWaterwatchData', () => { - it('Return false if no waterwatch flood levels are available', () => { - expect(hasWaterwatchData({ + describe('hasFloodLevels', () => { + it('Return false if no flood levels are available', () => { + expect(hasFloodLevels({ floodData: { floodLevels: null } })).toBeFalsy(); }); - it('return true if waterwatch flood levels are available', () => { - expect(hasWaterwatchData({ + it('return true if flood levels are available', () => { + expect(hasFloodLevels({ floodData: { floodLevels: { site_no: '07144100', @@ -105,17 +105,17 @@ describe('monitoring-location/selectors/flood-data-selector', () => { }); }); - describe('getWaterwatchFloodLevels', () => { + describe('getFloodLevels', () => { it('Expects null if no flood levels are defined', () => { - expect(getWaterwatchFloodLevels({ + expect(getFloodLevels({ floodData: { floodLevels: null } })).toBeNull(); }); - - it('Waterwatch flood levels are returned', () => { - expect(Object.values(getWaterwatchFloodLevels({ + + it('Flood levels are returned', () => { + expect(getFloodLevels({ floodData: { floodLevels: { site_no: '07144100', @@ -125,13 +125,19 @@ describe('monitoring-location/selectors/flood-data-selector', () => { major_flood_stage: '26' } } - }))).toEqual([20, 22.5, 25, 26]); + })).toEqual({ + site_no: '07144100', + action_stage: '20', + flood_stage: '22.5', + moderate_flood_stage: '25', + major_flood_stage: '26' + }); }); }); - describe('isWaterwatchVisible', () => { - it('Return false if waterwatch flood levels exist but no primary parameter exists', () => { - expect(isWaterwatchVisible({ + describe('showFloodLevels', () => { + it('Return false if flood levels exist but no primary parameter exists', () => { + expect(showFloodLevels({ floodData: { floodLevels: { site_no: '07144100', @@ -145,8 +151,8 @@ describe('monitoring-location/selectors/flood-data-selector', () => { })).toBeFalsy(); }); - it('Return false if waterwatch flood levels should not be visible due to parameter code', () => { - expect(isWaterwatchVisible({ + it('Return false if flood levels should not be visible due to parameter code', () => { + expect(showFloodLevels({ floodData: { floodLevels: { site_no: '07144100', @@ -166,8 +172,8 @@ describe('monitoring-location/selectors/flood-data-selector', () => { })).toBeFalsy(); }); - it('Return false if waterwatch flood levels should not be visible due to no flood levels', () => { - expect(isWaterwatchVisible({ + it('Return false if flood levels should not be visible due to no flood levels', () => { + expect(showFloodLevels({ floodData: { floodLevels: null }, @@ -181,8 +187,8 @@ describe('monitoring-location/selectors/flood-data-selector', () => { })).toBeFalsy(); }); - it('Return true if waterwatch flood levels should be visible', () => { - expect(isWaterwatchVisible({ + it('Return true if flood levels should be visible', () => { + expect(showFloodLevels({ floodData: { floodLevels: { site_no: '07144100', diff --git a/assets/src/scripts/monitoring-location/store/flood-inundation.js b/assets/src/scripts/monitoring-location/store/flood-data.js similarity index 80% rename from assets/src/scripts/monitoring-location/store/flood-inundation.js rename to assets/src/scripts/monitoring-location/store/flood-data.js index 3d98bcf659e1ab786080cc13a07400558350c343..ceae0dbbc58e02a612a5b7c5b65091f0893ef07c 100644 --- a/assets/src/scripts/monitoring-location/store/flood-inundation.js +++ b/assets/src/scripts/monitoring-location/store/flood-data.js @@ -1,5 +1,6 @@ -import {fetchFloodExtent, fetchFloodFeatures, fetchFIMPublicStatus, - fetchWaterwatchFloodLevels} from 'ui/web-services/flood-data'; +import {fetchFloodExtent, fetchFloodFeatures, + fetchFIMPublicStatus} from 'ui/web-services/fim-data'; +import {fetchFloodLevels} from 'ui/web-services/flood-levels'; const INITIAL_DATA = { stages: [], @@ -25,7 +26,7 @@ const setFloodFeatures = function(stages, extent) { * @param {String} siteno * @return {Function} which returns a Promise */ -const retrieveFloodData = function(siteno) { +export const retrieveFIMFloodData = function(siteno) { return function(dispatch) { const publicStatus = fetchFIMPublicStatus(siteno); const floodFeatures = fetchFloodFeatures(siteno); @@ -46,8 +47,6 @@ const retrieveFloodData = function(siteno) { }; }; - - /* * Slice reducer */ @@ -59,7 +58,7 @@ export const floodDataReducer = function(floodData=INITIAL_DATA, action) { stages: action.stages, extent: action.extent }; - case 'SET_WATERWACH_FLOOD_LEVELS': + case 'SET_FLOOD_LEVELS': return { ...floodData, floodLevels: action.floodLevels @@ -85,22 +84,22 @@ const setGageHeight = function(gageHeight) { * @param {JSON Object} floodLevels * @return {Object} Redux action */ -const setWaterwatchFloodLevels = function(floodLevels) { +const setFloodLevels = function(floodLevels) { return { - type: 'SET_WATERWACH_FLOOD_LEVELS', + type: 'SET_FLOOD_LEVELS', floodLevels }; }; /* - * Asynchronous Redux action to fetch the Waterwatch flood levels data + * Asynchronous Redux action to fetch the flood levels data * @param {String} siteno * @return {Function} which returns a Promise */ -const retrieveWaterwatchData = function(siteno) { +const retrieveFloodLevels = function(siteno) { return function(dispatch) { - return fetchWaterwatchFloodLevels(siteno).then(function(floodLevels) { - dispatch(setWaterwatchFloodLevels(floodLevels)); + return fetchFloodLevels(siteno).then(function(floodLevels) { + dispatch(setFloodLevels(floodLevels)); }); }; }; @@ -122,8 +121,8 @@ export const floodStateReducer = function(floodState={}, action) { export const Actions = { setFloodFeatures, - retrieveFloodData, + retrieveFIMFloodData, setGageHeight, - setWaterwatchFloodLevels, - retrieveWaterwatchData + setFloodLevels, + retrieveFloodLevels }; \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/store/flood-data.test.js b/assets/src/scripts/monitoring-location/store/flood-data.test.js new file mode 100644 index 0000000000000000000000000000000000000000..71568c3f8d49239ec3e10243161848835e7e57ce --- /dev/null +++ b/assets/src/scripts/monitoring-location/store/flood-data.test.js @@ -0,0 +1,179 @@ +import {applyMiddleware, combineReducers, createStore} from 'redux'; +import {default as thunk} from 'redux-thunk'; + +import {MOCK_WATERWATCH_FLOOD_LEVELS} from 'ui/mock-service-data'; + +import * as fimDataService from 'ui/web-services/fim-data'; +import * as floodLevelsService from 'ui/web-services/flood-levels'; + +import {Actions, floodDataReducer, floodStateReducer} from './flood-data'; + +describe('monitoring-location/store/flood-data module', () => { + /* eslint no-use-before-define: 0 */ + let store; + let fakeServer; + + beforeEach(() => { + store = createStore( + combineReducers({ + floodData: floodDataReducer, + floodState: floodStateReducer + }), + { + floodData: {}, + floodState: {} + }, + applyMiddleware(thunk) + ); + }); + + describe('floodDataReducer', () => { + describe('Actions.setFloodFeatures', () => { + it('Updates the flood features', () => { + store.dispatch(Actions.setFloodFeatures([1, 2, 3], { + xmin: -87.46671436884024, + ymin: 39.434393043085194, + xmax: -87.40838667928894, + ymax: 39.514453931168774 + })); + const floodData = store.getState().floodData; + + expect(floodData.stages).toEqual([1, 2, 3]); + expect(floodData.extent).toEqual({ + xmin: -87.46671436884024, + ymin: 39.434393043085194, + xmax: -87.40838667928894, + ymax: 39.514453931168774 + }); + }); + }); + + describe('Actions.retrieveFIMFloodData', () => { + + beforeEach(() => { + fimDataService.fetchFloodFeatures = + jest.fn().mockReturnValue(Promise.resolve(MOCK_STAGES)); + fimDataService.fetchFloodExtent = + jest.fn().mockReturnValue(Promise.resolve(MOCK_EXTENT)); + }) + + it('Expects a public site to populate the store', async() => { + fimDataService.fetchFIMPublicStatus = + jest.fn().mockReturnValue(Promise.resolve(true)); + let promise = store.dispatch(Actions.retrieveFIMFloodData('1234567')); + + return promise.then(() => { + const floodData = store.getState().floodData; + + expect(floodData.stages).toEqual([28, 29, 30]); + expect(floodData.extent).toEqual(MOCK_EXTENT.extent); + }); + }); + + it('Expects a site that is not public to not populate the store', () => { + fimDataService.fetchFIMPublicStatus = + jest.fn().mockReturnValue(Promise.resolve(false)); + let promise = store.dispatch(Actions.retrieveFIMFloodData('1234567')); + + return promise.then(() => { + const floodData = store.getState().floodData; + + expect(floodData.stages).toEqual([]); + expect(floodData.extent).toEqual({}); + }); + }); + }); + + describe('retrieveFloodLevels', () => { + const FLOOD_LEVELS = JSON.parse(MOCK_WATERWATCH_FLOOD_LEVELS); + + it('Expects the store to be updated on successful fetches', () => { + floodLevelsService.fetchFloodLevels = + jest.fn().mockReturnValue(Promise.resolve(FLOOD_LEVELS.sites[0])) + const promise = store.dispatch(Actions.retrieveFloodLevels('12345678')); + + return promise.then(() => { + const floodLevels = store.getState().floodData.floodLevels; + expect(floodLevels.action_stage) + .toEqual(FLOOD_LEVELS.sites[0].action_stage); + expect(floodLevels.flood_stage) + .toEqual(FLOOD_LEVELS.sites[0].flood_stage); + expect(floodLevels.moderate_flood_stage) + .toEqual(FLOOD_LEVELS.sites[0].moderate_flood_stage); + expect(floodLevels.major_flood_stage) + .toEqual(FLOOD_LEVELS.sites[0].major_flood_stage); + }); + }); + + it('Expects the store to contain empty flood levels if calls are unsuccessful', () => { + floodLevelsService.fetchFloodLevels = + jest.fn().mockReturnValue(Promise.resolve(null)); + const promise = store.dispatch(Actions.retrieveFloodLevels('12345678')); + + return promise.then(() => { + expect(store.getState().floodData.floodLevels).toEqual(null); + }); + }); + }); + + describe('setFloodLevels', () => { + const FLOOD_LEVELS = + { + site_no: '07144100', + action_stage: '20', + flood_stage: '22', + moderate_flood_stage: '25', + major_flood_stage: '26' + }; + + it('expect flood levels data to be updated', () => { + store.dispatch( + Actions.setFloodLevels(FLOOD_LEVELS)); + const floodLevels = store.getState().floodData.floodLevels; + + expect(floodLevels.action_stage).toEqual(FLOOD_LEVELS.action_stage); + expect(floodLevels.flood_stage).toEqual(FLOOD_LEVELS.flood_stage); + expect(floodLevels.moderate_flood_stage).toEqual(FLOOD_LEVELS.moderate_flood_stage); + expect(floodLevels.major_flood_stage).toEqual(FLOOD_LEVELS.major_flood_stage); + }); + }); + }); + + describe('floodStateReducer', () => { + describe('Actions.setGageHeight', () => { + it('Updates the gageHeight', () => { + store.dispatch(Actions.setGageHeight(12)); + + expect(store.getState().floodState.gageHeight).toBe(12); + }); + }); + }); +}); + +const MOCK_STAGES = + [{ + 'attributes': { + 'USGSID': '03341500', + 'STAGE': 30 + } + }, { + 'attributes': { + 'USGSID': '03341500', + 'STAGE': 29 + } + }, { + 'attributes': { + 'USGSID': '03341500', + 'STAGE': 28 + } + }]; + +const MOCK_EXTENT = { + extent: { + xmin: -87.46671436884024, + ymin: 39.434393043085194, + xmax: -87.40838667928894, + ymax: 39.514453931168774, + spatialReference: {wkid: 4326, latestWkid: 4326} + } +}; \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/store/flood-inundation.test.js b/assets/src/scripts/monitoring-location/store/flood-inundation.test.js deleted file mode 100644 index 11174288b76aa84c7feb585601cf5bc305d74551..0000000000000000000000000000000000000000 --- a/assets/src/scripts/monitoring-location/store/flood-inundation.test.js +++ /dev/null @@ -1,240 +0,0 @@ -import {applyMiddleware, combineReducers, createStore} from 'redux'; -import {default as thunk} from 'redux-thunk'; -import sinon from 'sinon'; - -import {MOCK_WATERWATCH_FLOOD_LEVELS} from 'ui/mock-service-data'; - -import {Actions, floodDataReducer, floodStateReducer} from './flood-inundation'; - -describe('monitoring-location/store/flood-inundation module', () => { - /* eslint no-use-before-define: 0 */ - let store; - let fakeServer; - - beforeEach(() => { - store = createStore( - combineReducers({ - floodData: floodDataReducer, - floodState: floodStateReducer - }), - { - floodData: {}, - floodState: {} - }, - applyMiddleware(thunk) - ); - fakeServer = sinon.createFakeServer(); - }); - - afterEach(() => { - fakeServer.restore(); - }); - - describe('floodDataReducer', () => { - describe('Actions.setFloodFeatures', () => { - it('Updates the flood features', () => { - store.dispatch(Actions.setFloodFeatures([1, 2, 3], { - xmin: -87.46671436884024, - ymin: 39.434393043085194, - xmax: -87.40838667928894, - ymax: 39.514453931168774 - })); - const floodData = store.getState().floodData; - - expect(floodData.stages).toEqual([1, 2, 3]); - expect(floodData.extent).toEqual({ - xmin: -87.46671436884024, - ymin: 39.434393043085194, - xmax: -87.40838667928894, - ymax: 39.514453931168774 - }); - }); - }); - - describe('Actions.retrieveFloodData', () => { - const PUBLIC_SITE = `{ - "features" :[{ - "attributes": { - "Public": 1, - "SITE_NO": "12345678" - } - }] - }`; - const NOT_PUBLIC_SITE = `{ - "features" :[{ - "attributes": { - "Public": 0, - "SITE_NO": "12345678" - } - }] - }`; - it('Expects call to determine FIM Public Status to occur', () => { - store.dispatch(Actions.retrieveFloodData('12345678')); - - expect(fakeServer.requests).toHaveLength(3); - const req1 = fakeServer.requests[0]; - const req2 = fakeServer.requests[1]; - const req3 = fakeServer.requests[2]; - expect(req1.url).toContain('12345678'); - expect(req1.url).toContain('outFields=PUBLIC%2CSITE_NO'); - - expect(req2.url).toContain('1234567'); - expect(req3.url).toContain('1234567'); - - expect(req2.url).toContain('outFields=USGSID%2C+STAGE'); - expect(req3.url).toContain('returnExtentOnly=true'); - }); - - it('Expects a public site with successful ajax calls to populate the store', () => { - let promise = store.dispatch(Actions.retrieveFloodData('1234567')); - fakeServer.requests[0].respond(200, {}, PUBLIC_SITE); - fakeServer.requests[1].respond(200, {}, MOCK_STAGES); - fakeServer.requests[2].respond(200, {}, MOCK_EXTENT); - - return promise.then(() => { - const floodData = store.getState().floodData; - - expect(floodData.stages).toEqual([28, 29, 30]); - expect(floodData.extent).toEqual(JSON.parse(MOCK_EXTENT).extent); - }); - }); - - it('Expects a not public site with successful ajax calls to populate the store', () => { - let promise = store.dispatch(Actions.retrieveFloodData('1234567')); - fakeServer.requests[0].respond(200, {}, NOT_PUBLIC_SITE); - fakeServer.requests[1].respond(200, {}, MOCK_STAGES); - fakeServer.requests[2].respond(200, {}, MOCK_EXTENT); - - return promise.then(() => { - const floodData = store.getState().floodData; - - expect(floodData.stages).toEqual([]); - expect(floodData.extent).toEqual({}); - }); - }); - }); - - describe('retrieveWaterwatchData', () => { - it('Expects that fetching urls have the siteno', () => { - store.dispatch(Actions.retrieveWaterwatchData('12345678')); - - expect(fakeServer.requests).toHaveLength(1); - expect(fakeServer.requests[0].url).toContain('12345678'); - }); - - it('Expects the store to be updated on successful fetches', () => { - const promise = store.dispatch(Actions.retrieveWaterwatchData('12345678')); - - fakeServer.requests[0].respond(200, {}, MOCK_WATERWATCH_FLOOD_LEVELS); - - return promise.then(() => { - const waterwatchData = store.getState().floodData; - expect(waterwatchData.floodLevels.action_stage) - .toEqual(JSON.parse(MOCK_WATERWATCH_FLOOD_LEVELS).sites[0].action_stage); - expect(waterwatchData.floodLevels.flood_stage) - .toEqual(JSON.parse(MOCK_WATERWATCH_FLOOD_LEVELS).sites[0].flood_stage); - expect(waterwatchData.floodLevels.moderate_flood_stage) - .toEqual(JSON.parse(MOCK_WATERWATCH_FLOOD_LEVELS).sites[0].moderate_flood_stage); - expect(waterwatchData.floodLevels.major_flood_stage) - .toEqual(JSON.parse(MOCK_WATERWATCH_FLOOD_LEVELS).sites[0].major_flood_stage); - }); - }); - - it('Expects the store to not contain empty features if calls are unsuccessful', () => { - - const promise = store.dispatch(Actions.retrieveWaterwatchData('12345678')); - - fakeServer.requests[0].respond(500, {}, 'Internal server error'); - - return promise.then(() => { - const waterwatchData = store.getState().floodData; - - expect(waterwatchData.floodLevels).toEqual(null); - }); - }); - }); - - describe('setWaterwatchFloodLevels', () => { - const FLOOD_LEVELS = [ - { - site_no: '07144100', - action_stage: '20', - flood_stage: '22', - moderate_flood_stage: '25', - major_flood_stage: '26' - } - ]; - - it('expect waterwatch data to be updated', () => { - store.dispatch( - Actions.setWaterwatchFloodLevels(FLOOD_LEVELS)); - const waterwatchData = store.getState().floodData; - - expect(waterwatchData.floodLevels[0].action_stage).toEqual(FLOOD_LEVELS[0].action_stage); - expect(waterwatchData.floodLevels[0].flood_stage).toEqual(FLOOD_LEVELS[0].flood_stage); - expect(waterwatchData.floodLevels[0].moderate_flood_stage).toEqual(FLOOD_LEVELS[0].moderate_flood_stage); - expect(waterwatchData.floodLevels[0].major_flood_stage).toEqual(FLOOD_LEVELS[0].major_flood_stage); - }); - }); - }); - - describe('floodStateReducer', () => { - describe('Actions.setGageHeight', () => { - it('Updates the gageHeight', () => { - store.dispatch(Actions.setGageHeight(12)); - - expect(store.getState().floodState.gageHeight).toBe(12); - }); - }); - }); -}); - -const MOCK_STAGES = ` -{ - "displayFieldName": "USGSID", - "fieldAliases": { - "USGSID": "USGSID", - "STAGE": "STAGE" - }, - "fields": [{ - "name": "USGSID", - "type": "esriFieldTypeString", - "alias": "USGSID", - "length": 254 - }, - { - "name": "STAGE", - "type": "esriFieldTypeDouble", - "alias": "STAGE" - } - ], - "features": [{ - "attributes": { - "USGSID": "03341500", - "STAGE": 30 - } - }, - { - "attributes": { - "USGSID": "03341500", - "STAGE": 29 - } - }, - { - "attributes": { - "USGSID": "03341500", - "STAGE": 28 - } - } - ] -}`; - -const MOCK_EXTENT = `{ - "extent": { - "xmin": -87.46671436884024, - "ymin": 39.434393043085194, - "xmax": -87.40838667928894, - "ymax": 39.514453931168774, - "spatialReference": {"wkid": 4326, "latestWkid": 4326} - } -}`; \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/store/hydrograph-data.test.js b/assets/src/scripts/monitoring-location/store/hydrograph-data.test.js index ed7b2561390ce1cefa4a6b8e25059fc0c41416b5..5ea5d7a93fcfd3146af58ae0eab8f0ed0029d204 100644 --- a/assets/src/scripts/monitoring-location/store/hydrograph-data.test.js +++ b/assets/src/scripts/monitoring-location/store/hydrograph-data.test.js @@ -2,7 +2,6 @@ import mockConsole from 'jest-mock-console'; import * as luxon from 'luxon'; import {applyMiddleware, combineReducers, createStore} from 'redux'; import {default as thunk} from 'redux-thunk'; -import sinon from 'sinon'; import { MOCK_IV_DATA, @@ -29,7 +28,6 @@ import { describe('monitoring-location/store/hydrograph-data', () => { let store; - let fakeServer; let restoreConsole; config.locationTimeZone = 'America/Chicago'; @@ -45,12 +43,10 @@ describe('monitoring-location/store/hydrograph-data', () => { }, applyMiddleware(thunk) ); - fakeServer = sinon.createFakeServer(); restoreConsole = mockConsole(); }); afterEach(() => { - fakeServer.restore(); restoreConsole(); }); diff --git a/assets/src/scripts/monitoring-location/store/hydrograph-parameters.js b/assets/src/scripts/monitoring-location/store/hydrograph-parameters.js index b867b0a76a1ea7e1e3b94ecd0ea561cdb89bf942..5b63103a6a8bc86c9bc657050bf3c463db3429f0 100644 --- a/assets/src/scripts/monitoring-location/store/hydrograph-parameters.js +++ b/assets/src/scripts/monitoring-location/store/hydrograph-parameters.js @@ -7,7 +7,7 @@ import {fetchGroundwaterLevels} from 'ui/web-services/groundwater-levels'; import {fetchTimeSeries} from 'ui/web-services/instantaneous-values'; import {getConvertedTemperatureParameter, hasMeasuredFahrenheitParameter} from 'ml/iv-data-utils'; -import {Actions as floodStateActions} from './flood-inundation'; +import {Actions as floodStateActions} from './flood-data'; /* * Synchronous Redux action - updatethe hydrograph variables diff --git a/assets/src/scripts/monitoring-location/store/index.js b/assets/src/scripts/monitoring-location/store/index.js index 781710d7d38cf90a84a7acf235177743534cff0b..773a32fe5b23616647f30c3bfe746281aa7e04cc 100644 --- a/assets/src/scripts/monitoring-location/store/index.js +++ b/assets/src/scripts/monitoring-location/store/index.js @@ -5,7 +5,7 @@ import {dailyValueTimeSeriesDataReducer as dailyValueTimeSeriesData} from './dai import {dailyValueTimeSeriesStateReducer as dailyValueTimeSeriesState} from './daily-value-time-series'; import { floodDataReducer as floodData, - floodStateReducer as floodState} from './flood-inundation'; + floodStateReducer as floodState} from './flood-data'; import {hydrographDataReducer as hydrographData} from './hydrograph-data'; import {hydrographParametersReducer as hydrographParameters, initializeParameters} from './hydrograph-parameters'; diff --git a/assets/src/scripts/web-services/camera-images.test.js b/assets/src/scripts/web-services/camera-images.test.js index 0272bf9e57c9de762f64480e0d60cc9a6deabe04..9db28923cdf671501c5bc7335be4c86eb04f68ac 100644 --- a/assets/src/scripts/web-services/camera-images.test.js +++ b/assets/src/scripts/web-services/camera-images.test.js @@ -1,7 +1,7 @@ import {enableFetchMocks, disableFetchMocks} from 'jest-fetch-mock'; import mockConsole from 'jest-mock-console'; -import {MOCK_CAMERA_METADATA} from '../mock-service-data'; +import {MOCK_CAMERA_METADATA} from 'ui/mock-service-data'; import {fetchCameraMetaData} from './camera-images'; @@ -23,10 +23,11 @@ describe('web-services/camera-images', () => { afterEach(() => { restoreConsole(); - }) + }); + it('puts the site number in the request url and returns valid response data', async() => { fetch.once(MOCK_CAMERA_METADATA, {status: 200}); - let resp = await fetchCameraMetaData('05428500'); + const resp = await fetchCameraMetaData('05428500'); expect(fetch.mock.calls).toHaveLength(1); expect(fetch.mock.calls[0][0]).toContain('siteID=05428500'); @@ -36,21 +37,21 @@ describe('web-services/camera-images', () => { it('Successful request with no data returns an object with success set to false', async() => { fetch.once('No data returned', {status: 200}); - const resp= await fetchCameraMetaData('05428500'); + const resp = await fetchCameraMetaData('05428500'); expect(resp).toEqual({success: false}); }); it('handles a 400 bad status', async() => { fetch.once('{}', {status: 400}); - const resp= await fetchCameraMetaData('05428500'); + const resp = await fetchCameraMetaData('05428500'); expect(resp).toEqual({success: false}); }); it('handles a bad fetch', async() => { fetch.mockReject(new Error('fake error message')); - const resp= await fetchCameraMetaData('05428500'); + const resp = await fetchCameraMetaData('05428500'); expect(resp).toEqual({success: false}); }); diff --git a/assets/src/scripts/web-services/fim-data.js b/assets/src/scripts/web-services/fim-data.js new file mode 100644 index 0000000000000000000000000000000000000000..f6ba492e938b983368972fd42e0a40d76aaf074b --- /dev/null +++ b/assets/src/scripts/web-services/fim-data.js @@ -0,0 +1,78 @@ +import config from 'ui/config'; + +const FLOOD_SITES_ENDPOINT = `${config.FIM_GIS_ENDPOINT}sites/MapServer/`; +const FLOOD_EXTENTS_ENDPOINT = `${config.FIM_GIS_ENDPOINT}floodExtents/MapServer/`; + +/* + * Determine if a site has public FIM data + * @param {String} siteno + * @return {Promise} resolves to a boolean, true if public, false otherwise + */ +export const fetchFIMPublicStatus = async function(siteno) { + const query = `where=SITE_NO='${siteno}'&outFields=PUBLIC,SITE_NO&f=json`; + const url = `${FLOOD_SITES_ENDPOINT}0/query?${encodeURI(query)}`; + try { + const response = await fetch(url, { + method: 'GET' + }); + if (response.status === 200) { + const respJSON = await response.json(); + return respJSON.features[0].attributes.Public > 0; + } else { + console.error(`Received bad status, ${response.status} from ${url}`); + return false; + } + } catch(error) { + console.error(`Failed fetch for ${url}`); + return false; + } +}; + +/* + * Retrieve flood features if any for siteno + * @param {String} siteno + * @return {Promise} resolves to an array of features for the site + */ +export const fetchFloodFeatures = async function(siteno) { + const query = `where=USGSID = '${siteno}'&outFields=USGSID,STAGE&returnGeometry=false&f=json`; + const url = `${FLOOD_EXTENTS_ENDPOINT}0/query?${encodeURI(query)}`; + try { + const response = await fetch(url, { + method: 'GET' + }); + if (response.status === 200) { + const respJSON = await response.json(); + return respJSON.features ? respJSON.features : []; + } else { + console.error(`Received bad status, ${response.status} for ${url}`); + return []; + } + } catch(error) { + console.error(`Failed fetch for ${url}`); + return []; + } +}; + +/* + * Retrieve the extent of the flood information for siteno + * @param {String} siteno + * @return {Promise} resolves to the extent Object or the empty object if any errors + */ +export const fetchFloodExtent = async function(siteno) { + const query = `where=USGSID = '${siteno}'&returnExtentOnly=true&outSR=4326&f=json` + const url = `${FLOOD_EXTENTS_ENDPOINT}0/query?${encodeURI(query)}`; + try { + const response = await fetch(url, { + method: 'GET' + }); + if (response.status === 200) { + return await response.json(); + } else { + console.error(`Received bad status, ${response.status} for ${url}`) + return {}; + } + } catch(error) { + console.error(`Failed fetch for ${url}`); + return {}; + } +}; diff --git a/assets/src/scripts/web-services/fim-data.test.js b/assets/src/scripts/web-services/fim-data.test.js new file mode 100644 index 0000000000000000000000000000000000000000..b79872abab532e7073039a57a94ea1a05441e07b --- /dev/null +++ b/assets/src/scripts/web-services/fim-data.test.js @@ -0,0 +1,200 @@ +import {disableFetchMocks, enableFetchMocks} from "jest-fetch-mock"; +import mockConsole from 'jest-mock-console'; + +import {fetchFIMPublicStatus, fetchFloodExtent, fetchFloodFeatures} from './fim-data'; + + +describe('web-services/fim-data', () => { + beforeEach(() => { + enableFetchMocks(); + }); + + afterEach(() => { + disableFetchMocks(); + }); + + describe('web-services/fetchFIMPublicStatus', () => { + const siteno = '12345678'; + let restoreConsole; + beforeEach(() => { + fetch.resetMocks(); + restoreConsole = mockConsole(); + }); + + afterEach(() => { + restoreConsole(); + }); + + it('Expects response true if valid response and response indicates the site is public', async () => { + fetch.once(`{ + "features" :[{ + "attributes": { + "Public": 1, + "SITE_NO": "12345678" + } + }] + }`, {status: 200}); + const resp = await fetchFIMPublicStatus('12345678'); + + expect(fetch.mock.calls).toHaveLength(1); + expect(fetch.mock.calls[0][0]).toContain('12345678'); + expect(resp).toBeTruthy(); + }); + + it('Expects response false if valid response indicates the status is not public', async () => { + fetch.once(`{ + "features" :[{ + "attributes": { + "Public": 0, + "SITE_NO": "12345678" + } + }] + }`, {status: 200}); + const resp = await fetchFIMPublicStatus('12345678'); + + expect(resp).toBeFalsy(); + }); + + it('handles a 400 bad status', async () => { + fetch.once('{}', {status: 400}); + const resp = await fetchFIMPublicStatus('12345678'); + + expect(resp).toBeFalsy(); + }); + + it('handles a bad fetch', async () => { + fetch.mockReject(new Error('fake error message')); + const resp = await fetchFIMPublicStatus('12345678'); + + expect(resp).toBeFalsy(); + }); + }); + + describe('web-services/fetchFloodFeatures', () => { + let restoreConsole; + beforeEach(() => { + fetch.resetMocks(); + restoreConsole = mockConsole(); + }); + + afterEach(() => { + restoreConsole(); + }); + + it('Expects valid response to return features if in response', async () => { + fetch.once(MOCK_FLOOD_FEATURE, {status: 200}); + const resp = await fetchFloodFeatures('12345678'); + + expect(fetch.mock.calls).toHaveLength(1); + expect(fetch.mock.calls[0][0]).toContain('12345678'); + expect(resp).toEqual(JSON.parse(MOCK_FLOOD_FEATURE).features); + }); + + it('Expects valid response with no features to return an emty array', async () => { + fetch.once('{}', {status: 200}); + const resp = await fetchFloodFeatures('12345678'); + + expect(resp).toHaveLength(0); + }); + + it('handles a 400 bad status', async () => { + fetch.once('{}', {status: 400}); + const resp = await fetchFloodFeatures('12345678') + + expect(resp).toHaveLength(0); + }); + + it('handles a bad fetch', async () => { + fetch.mockReject(new Error('fake error message')); + const resp = await await fetchFloodFeatures('12345678') + + expect(resp).toHaveLength(0); + }); + }); + + describe('web-services/fetchFloodExtent', () => { + let restoreConsole; + beforeEach(() => { + fetch.resetMocks(); + restoreConsole = mockConsole(); + }); + + afterEach(() => { + restoreConsole(); + }); + + it('Expects a valid response to return the payload', async () => { + fetch.once(MOCK_FLOOD_EXTENT, {status: 200}); + const resp = await fetchFloodExtent('12345678'); + + expect(fetch.mock.calls).toHaveLength(1); + expect(fetch.mock.calls[0][0]).toContain('12345678'); + expect(resp).toEqual(JSON.parse(MOCK_FLOOD_EXTENT)); + }); + + it('handles a 400 bad status', async () => { + fetch.once('{}', {status: 400}); + const resp = await fetchFloodExtent('12345678'); + + expect(resp).toEqual({}); + }); + + it('handles a bad fetch', async () => { + fetch.mockReject(new Error('fake error message')); + const resp = await fetchFloodExtent('12345678'); + + expect(resp).toEqual({}); + }); + }); +}); + +const MOCK_FLOOD_FEATURE = ` +{ + "displayFieldName": "USGSID", + "fieldAliases": { + "USGSID": "USGSID", + "STAGE": "STAGE" + }, + "fields": [{ + "name": "USGSID", + "type": "esriFieldTypeString", + "alias": "USGSID", + "length": 254 + }, { + "name": "STAGE", + "type": "esriFieldTypeDouble", + "alias": "STAGE" + }], + "features": [{ + "attributes": { + "USGSID": "03341500", + "STAGE": 30 + } + }, { + "attributes": { + "USGSID": "03341500", + "STAGE": 29 + } + }, { + "attributes": { + "USGSID": "03341500", + "STAGE": 28 + } + }] +} +`; + +const MOCK_FLOOD_EXTENT = ` +{ + "extent": { + "xmin": -84.353211731250525, + "ymin": 34.016663666167332, + "xmax": -84.223456338038901, + "ymax": 34.100999075364072, + "spatialReference": { + "wkid": 4326, + "latestWkid": 4326 + } + } +} +`; diff --git a/assets/src/scripts/web-services/flood-data.js b/assets/src/scripts/web-services/flood-data.js deleted file mode 100644 index 30f7d7f52f149caf4961b2b500f15e7c3174765a..0000000000000000000000000000000000000000 --- a/assets/src/scripts/web-services/flood-data.js +++ /dev/null @@ -1,87 +0,0 @@ -import {get} from 'ui/ajax'; -import config from 'ui/config'; - -export const FLOOD_SITES_ENDPOINT = `${config.FIM_GIS_ENDPOINT}sites/MapServer/`; -export const FLOOD_EXTENTS_ENDPOINT = `${config.FIM_GIS_ENDPOINT}floodExtents/MapServer/`; - -const WATERWATCH_URL = config.WATERWATCH_ENDPOINT; -const FORMAT = 'json'; - -/* - * Determine if a site has public FIM data - * @param {String} siteno - * @return {Promise} resolves to a boolean, true if public, false otherwise - */ -export const fetchFIMPublicStatus = function(siteno) { - const FIM_SITE_QUERY = `${FLOOD_SITES_ENDPOINT}/0/query?where=SITE_NO%3D%27${siteno}%27&text=&objectIds=&time=&geometry=&geometryType=esriGeometryEnvelope&inSR=&spatialRel=esriSpatialRelIntersects&relationParam=&outFields=PUBLIC%2CSITE_NO&returnGeometry=false&returnTrueCurves=false&maxAllowableOffset=&geometryPrecision=&outSR=&returnIdsOnly=false&returnCountOnly=false&orderByFields=&groupByFieldsForStatistics=&outStatistics=&returnZ=false&returnM=false&gdbVersion=&returnDistinctValues=false&resultOffset=&resultRecordCount=&f=pjson`; - return get(FIM_SITE_QUERY) - .then((response) => { - const respJson = JSON.parse(response); - return respJson.features[0].attributes.Public > 0; - }) - .catch(reason => { - console.log(`Unable to get FIM Public Status data for ${siteno} with reason: ${reason}`); - return false; - }); -}; - -/* - * Retrieve flood features if any for siteno - * @param {String} siteno - * @return {Promise} resolves to an array of features for the site - */ -export const fetchFloodFeatures = function(siteno) { - const FIM_QUERY = `${FLOOD_EXTENTS_ENDPOINT}/0/query?where=USGSID+%3D+%27${siteno}%27&outFields=USGSID%2C+STAGE&returnGeometry=false&returnTrueCurves=false&returnIdsOnly=false&returnCountOnly=false&returnZ=false&returnM=falsereturnDistinctValues=false&f=json`; - - return get(FIM_QUERY) - .then((response) => { - const respJson = JSON.parse(response); - return respJson.features ? respJson.features : []; - }) - .catch(reason => { - console.log(`Unable to get FIM stages for ${siteno} with reason: ${reason}`); - return []; - }); -}; -/* - * Retrieve the extent of the flood information for siteno - * @param {String} siteno - * @return {Promise} resolves to the extent Object or the empty object if an errors - */ -export const fetchFloodExtent = function(siteno) { - const FIM_QUERY = `${FLOOD_EXTENTS_ENDPOINT}/0/query?where=USGSID+%3D+%27${siteno}%27&returnExtentOnly=true&outSR=4326&f=json`; - return get(FIM_QUERY) - .then((response) => { - return JSON.parse(response); - }) - .catch(reason => { - console.log(`Unable to get FIM extents for ${siteno} with reason: ${reason}`); - return {}; - }); -}; - -/* - * Retrieve waterwatch flood levels any for siteno - * @param {String} siteno - * @return {Promise} resolves to an array of features for the site - */ -const fetchWaterwatchData = function(waterwatchQuery, siteno) { - return get(waterwatchQuery) - .then((responseText) => { - const response = JSON.parse(responseText); - if (!response.sites || !response.sites.length) { - return null; - } - return response.sites[0]; - }) - .catch(reason => { - console.log(`Unable to get Waterwatch data for ${siteno} with reason: ${reason}`); - return null; - }); -}; - -// waterwatch webservice calls -export const fetchWaterwatchFloodLevels = function(siteno) { - const waterwatchQuery = `${WATERWATCH_URL}/floodstage?format=${FORMAT}&site=${siteno}`; - return fetchWaterwatchData(waterwatchQuery, siteno); -}; diff --git a/assets/src/scripts/web-services/flood-data.test.js b/assets/src/scripts/web-services/flood-data.test.js deleted file mode 100644 index f12abc46159172be037d5cd44306426680caddb9..0000000000000000000000000000000000000000 --- a/assets/src/scripts/web-services/flood-data.test.js +++ /dev/null @@ -1,250 +0,0 @@ -import sinon from 'sinon'; - -import {MOCK_WATERWATCH_FLOOD_LEVELS} from 'ui/mock-service-data'; - -import {fetchFIMPublicStatus, fetchFloodExtent, fetchFloodFeatures, - fetchWaterwatchFloodLevels} from './flood-data'; - - -describe('web-services/flood-data', () => { - let fakeServer; - beforeEach(() => { - fakeServer = sinon.createFakeServer(); - }); - - afterEach(() => { - fakeServer.restore(); - }); - - describe('fetchFIMPublicStatus', () => { - const siteno = '12345678'; - describe('with valid response', () => { - let promise; - - it('expected response is true', () => { - promise = fetchFIMPublicStatus(siteno); - fakeServer.requests[0].respond( - 200, - {'Content-Type': 'application/json'}, - `{ - "features" :[{ - "attributes": { - "Public": 1, - "SITE_NO": "12345678" - } - }] - }` - ); - - return promise.then((resp) => { - expect(resp).toBeTruthy(); - }); - }); - - it('expected response is False', () => { - promise = fetchFIMPublicStatus(siteno); - fakeServer.requests[0].respond( - 200, - { - 'Content-Type': 'appliation/json' - }, - `{ - "features": [{ - "attributes": { - "Public": 0, - "SITE_NO": "12345678" - } - }] - }` - ); - - return promise.then((resp) => { - expect(resp).toBeFalsy(); - }); - }); - }); - - describe('with error response', () => { - it('On failed response return false', () => { - const fetchPromise = fetchFIMPublicStatus(siteno); - fakeServer.requests[0].respond(500); - return fetchPromise.then((resp) => { - expect(resp).toBeFalsy(); - }); - }); - }); - }); - - describe('fetchFloodFeatures', () => { - const siteno = '12345678'; - - describe('with valid response', () => { - let promise; - - beforeEach(() => { - /* eslint no-use-before-define: 0 */ - promise = fetchFloodFeatures(siteno); - fakeServer.requests[0].respond( - 200, - { - 'Content-Type': 'application/json' - }, - MOCK_FLOOD_FEATURE - ); - }); - - it('expected response is json object with the stages', () => { - return promise.then((resp) => { - expect(resp).toHaveLength(3); - expect(resp[0].attributes.STAGE).toBe(30); - expect(resp[1].attributes.STAGE).toBe(29); - expect(resp[2].attributes.STAGE).toBe(28); - }); - }); - }); - - describe('with error response', () => { - it('On failed response return an empty feature list', () => { - const fetchPromise = fetchFloodFeatures(siteno); - fakeServer.requests[0].respond(500); - return fetchPromise.then((resp) => { - expect(resp).toHaveLength(0); - }); - }); - }); - }); - - describe('web-services/fetchFloodExtent', () => { - let promise; - const siteno = '12345678'; - - describe('with valid response', () => { - - beforeEach(() => { - /* eslint no-use-before-define: 0 */ - promise = fetchFloodExtent(siteno); - fakeServer.requests[0].respond( - 200, - { - 'Content-Type': 'application/json' - }, - MOCK_FLOOD_EXTENT - ); - }); - - it('expected response is json object with the extent', () => { - return promise.then((resp) => { - expect(resp.extent).toBeDefined(); - expect(resp.extent.xmin).toBe(-84.353211731250525); - expect(resp.extent.xmax).toBe(-84.223456338038901); - expect(resp.extent.ymin).toBe(34.016663666167332); - expect(resp.extent.ymax).toBe(34.100999075364072); - }); - }); - }); - - describe('with error response', () => { - beforeEach(() => { - /* eslint no-use-before-define: 0 */ - promise = fetchFloodExtent(siteno); - fakeServer.requests[0].respond(500); - }); - - it('On failed response return an empty feature list', () => { - return promise.then((resp) => { - expect(resp).toEqual({}); - }); - }); - }); - }); - - describe('web-services/fetchWaterwatchFloodLevels', () => { - let floodLevelPromise; - const siteno = '07144100'; - - describe('with valid response', () => { - - beforeEach(() => { - /* eslint no-use-before-define: 0 */ - - floodLevelPromise = fetchWaterwatchFloodLevels(siteno); - - fakeServer.requests[0].respond( - 200, - { - 'Content-Type': 'application/json' - }, - MOCK_WATERWATCH_FLOOD_LEVELS - ); - }); - - it('expected response is json object with the flood levels', () => { - return floodLevelPromise.then((resp) => { - expect(resp).not.toEqual(null); - expect(resp.site_no).toBe('07144100'); - }); - }); - }); - - describe('with error response', () => { - it('On failed response return an empty flood levels list', () => { - const fetchPromise = fetchWaterwatchFloodLevels(siteno); - fakeServer.requests[0].respond(500, {}, 'Error'); - return fetchPromise.then((resp) => { - expect(resp).toBeNull(); - }); - }); - }); - }); -}); - -const MOCK_FLOOD_FEATURE = ` -{ - "displayFieldName": "USGSID", - "fieldAliases": { - "USGSID": "USGSID", - "STAGE": "STAGE" - }, - "fields": [{ - "name": "USGSID", - "type": "esriFieldTypeString", - "alias": "USGSID", - "length": 254 - }, { - "name": "STAGE", - "type": "esriFieldTypeDouble", - "alias": "STAGE" - }], - "features": [{ - "attributes": { - "USGSID": "03341500", - "STAGE": 30 - } - }, { - "attributes": { - "USGSID": "03341500", - "STAGE": 29 - } - }, { - "attributes": { - "USGSID": "03341500", - "STAGE": 28 - } - }] -} -`; - -const MOCK_FLOOD_EXTENT = ` -{ - "extent": { - "xmin": -84.353211731250525, - "ymin": 34.016663666167332, - "xmax": -84.223456338038901, - "ymax": 34.100999075364072, - "spatialReference": { - "wkid": 4326, - "latestWkid": 4326 - } - } -} -`; diff --git a/assets/src/scripts/web-services/flood-levels.js b/assets/src/scripts/web-services/flood-levels.js new file mode 100644 index 0000000000000000000000000000000000000000..ad6d5a74b23027e84af7a3e5549eea07881dda3f --- /dev/null +++ b/assets/src/scripts/web-services/flood-levels.js @@ -0,0 +1,25 @@ +import config from 'ui/config'; + +/* + * Retrieve NOAA flood levels from the water watch endpointany for siteno + * @param {String} siteno + * @return {Promise} resolves to an Object containing the available flood levels or null if none. + */ +export const fetchFloodLevels = async function(siteno) { + const url = `${config.WATERWATCH_ENDPOINT}/floodstage?site=${siteno}&format=json`; + try { + const response = await fetch(url, { + method: 'GET' + }); + if (response.status === 200) { + const respJSON = await response.json(); + return respJSON.sites && respJSON.sites.length ? respJSON.sites[0] : null; + } else { + console.error(`Received bad status, ${response.status} for ${url}`) + return null; + } + } catch(error) { + console.error(`Failed fetch for ${url}`); + return null; + } +} diff --git a/assets/src/scripts/web-services/flood-levels.test.js b/assets/src/scripts/web-services/flood-levels.test.js new file mode 100644 index 0000000000000000000000000000000000000000..c37c2179c2c28742fe0cfed752a7f8e3b8979882 --- /dev/null +++ b/assets/src/scripts/web-services/flood-levels.test.js @@ -0,0 +1,58 @@ +import {enableFetchMocks, disableFetchMocks} from 'jest-fetch-mock'; +import mockConsole from 'jest-mock-console'; + +import {MOCK_WATERWATCH_FLOOD_LEVELS} from 'ui/mock-service-data'; + +import {fetchFloodLevels} from './flood-levels'; + +describe('web-services/flood-levels', () => { + beforeEach(() => { + enableFetchMocks(); + }); + + afterEach(() => { + disableFetchMocks(); + }); + + describe('fetchFloodLevels', () => { + let restoreConsole; + beforeEach(() => { + fetch.resetMocks(); + restoreConsole = mockConsole(); + }); + + afterEach(() => { + restoreConsole(); + }); + + it('Valid response with flood levels returns the flood level object', async () => { + fetch.once(MOCK_WATERWATCH_FLOOD_LEVELS, {status: 200}); + const resp = await fetchFloodLevels('12345678'); + + expect(fetch.mock.calls).toHaveLength(1); + expect(fetch.mock.calls[0][0]).toContain('site=12345678'); + expect(resp).toEqual(JSON.parse(MOCK_WATERWATCH_FLOOD_LEVELS).sites[0]); + }); + + it('valid response without flood levels returns null', async() => { + fetch.once('{sites:[]}', {status: 200}); + const resp = await fetchFloodLevels('12345678'); + + expect(resp).toBeNull(); + }); + + it('handles a 400 bad status', async() => { + fetch.once('{}', {status: 400}); + const resp = await fetchFloodLevels('12345678'); + + expect(resp).toBeNull(); + }); + + it('handles a bad fetch', async() => { + fetch.mockReject(new Error('fake error message')); + const resp = await fetchFloodLevels('12345678'); + + expect(resp).toBeNull(); + }); + }); +}); \ No newline at end of file diff --git a/assets/src/styles/components/hydrograph/_graph.scss b/assets/src/styles/components/hydrograph/_graph.scss index 43eb0822f14219c0d4a09a63ff795a00794ae06c..1a0d741584d2f945a5d9b2c3493a745413cebf9e 100644 --- a/assets/src/styles/components/hydrograph/_graph.scss +++ b/assets/src/styles/components/hydrograph/_graph.scss @@ -211,7 +211,7 @@ svg { stroke: #ff2600; } - .waterwatch-data-series { + .flood-levels-series { fill: none; stroke-width: 1px; stroke: #001aff;