);
diff --git a/src/js/components/search/visualizations/screens/NoFiltersScreen.jsx b/src/js/components/search/visualizations/screens/NoFiltersScreen.jsx
new file mode 100644
index 0000000000..6e11ecfcf9
--- /dev/null
+++ b/src/js/components/search/visualizations/screens/NoFiltersScreen.jsx
@@ -0,0 +1,23 @@
+/**
+ * NoFiltersScreen.jsx
+ * Created by Kevin Li 12/26/17
+ */
+
+import React from 'react';
+
+import { CircleArrowLeft } from 'components/sharedComponents/icons/Icons';
+
+const NoFiltersScreen = () => (
+
+
+
+
+
+
+ Choose your filters and submit your search to begin.
+
+
+
+);
+
+export default NoFiltersScreen;
diff --git a/src/js/components/search/visualizations/time/TimeVisualization.jsx b/src/js/components/search/visualizations/time/TimeVisualization.jsx
index a211236c4b..faed7f6c84 100644
--- a/src/js/components/search/visualizations/time/TimeVisualization.jsx
+++ b/src/js/components/search/visualizations/time/TimeVisualization.jsx
@@ -6,9 +6,13 @@
import React from 'react';
import PropTypes from 'prop-types';
+import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
+
import BarChart from './chart/BarChart';
import Tooltip from './TimeVisualizationTooltip';
-import ChartMessage from './TimeVisualizationChartMessage';
+import ChartLoadingMessage from '../ChartLoadingMessage';
+import ChartNoResults from '../ChartNoResults';
+import ChartError from '../ChartError';
const defaultProps = {
groups: [],
@@ -43,7 +47,9 @@ const propTypes = {
xSeries: PropTypes.array,
ySeries: PropTypes.array,
loading: PropTypes.bool,
- legend: PropTypes.array
+ legend: PropTypes.array,
+ visualizationPeriod: PropTypes.string,
+ error: PropTypes.bool
};
/* eslint-enable react/no-unused-prop-types */
@@ -81,10 +87,13 @@ export default class TimeVisualization extends React.Component {
barWidth={this.state.barWidth} />);
}
- let chart = ();
+ let chart = ();
if (this.props.loading) {
// API request is still pending
- chart = ();
+ chart = ();
+ }
+ else if (this.props.error) {
+ chart = ();
}
else if (this.props.groups.length > 0) {
// only mount the chart component if there is data to display
@@ -96,7 +105,13 @@ export default class TimeVisualization extends React.Component {
return (
- {chart}
+
+ {chart}
+
{tooltip}
);
diff --git a/src/js/components/search/visualizations/time/TimeVisualizationSection.jsx b/src/js/components/search/visualizations/time/TimeVisualizationSection.jsx
index 22593597d5..16cca363f2 100644
--- a/src/js/components/search/visualizations/time/TimeVisualizationSection.jsx
+++ b/src/js/components/search/visualizations/time/TimeVisualizationSection.jsx
@@ -12,7 +12,8 @@ import TimeVisualizationPeriodButton from './TimeVisualizationPeriodButton';
const propTypes = {
data: PropTypes.object,
- updateVisualizationPeriod: PropTypes.func
+ updateVisualizationPeriod: PropTypes.func,
+ visualizationPeriod: PropTypes.string
};
export default class TimeVisualizationSection extends React.Component {
diff --git a/src/js/components/search/visualizations/time/chart/BarChart.jsx b/src/js/components/search/visualizations/time/chart/BarChart.jsx
index 068b3aefca..9770a753e4 100644
--- a/src/js/components/search/visualizations/time/chart/BarChart.jsx
+++ b/src/js/components/search/visualizations/time/chart/BarChart.jsx
@@ -22,12 +22,14 @@ const propTypes = {
width: PropTypes.number,
height: PropTypes.number,
xSeries: PropTypes.array,
+ rawLabels: PropTypes.array,
ySeries: PropTypes.array,
showTooltip: PropTypes.func,
enableHighlight: PropTypes.bool,
padding: PropTypes.object,
legend: PropTypes.array,
- activeLabel: PropTypes.object
+ activeLabel: PropTypes.object,
+ visualizationPeriod: PropTypes.string
};
/* eslint-enable react/no-unused-prop-types */
@@ -241,6 +243,7 @@ export default class BarChart extends React.Component {
graphHeight,
yValues: allY,
xValues: props.groups,
+ rawLabels: props.rawLabels,
yAverage: mean(allY),
yTicks: yScale.ticks(7)
});
@@ -408,9 +411,11 @@ export default class BarChart extends React.Component {
width={this.props.width - this.props.padding.left}
padding={this.props.padding}
data={this.state.xValues}
+ rawLabels={this.state.rawLabels}
scale={this.state.xScale}
axisPos={this.state.xAxisPos}
- activeLabel={this.props.activeLabel} />
+ activeLabel={this.props.activeLabel}
+ visualizationPeriod={this.props.visualizationPeriod} />
{
- // offset the D3 calculated position by the left padding
- // and put the label in the middle
- // of the each tick's width to center the text
- if (item !== props.activeLabel.xValue) {
- return null;
- }
- const xPos = props.scale(item) + (props.scale.bandwidth() / 2);
- return ();
- })
- );
+ const xPos = props.scale(props.activeLabel.xValue) + (props.scale.bandwidth() / 2);
+ return ([]);
+ }
+
+ // Figure out which labels to show depending on type
+ let labelIterator = 1;
+ let labelOffset = 0;
+ // Year has 4 quarters
+ if (props.visualizationPeriod === "quarter") {
+ labelIterator = 4;
}
- else if (ref.length === 1) {
- return (
- props.data.map((item) => {
- // offset the D3 calculated position by the left padding and put the label in
- // the middle
- // of the each tick's width to center the text
- const xPos = props.scale(item) + (props.scale.bandwidth() / 2);
- return ();
- })
- );
+ else if (props.visualizationPeriod === "month") {
+ labelIterator = 12;
}
- else if (ref[0][0] === 'Q') {
- // Quarterly
- return (
- props.data.map((item, index) => {
- // offset the D3 calculated position by the left padding and put the label in the middle
- // of the each tick's width to center the text
- if (index % 4 !== 0) {
- return null;
- }
-
- const endIndex = index + 3 > props.data.length ? props.data.length - 1 : index + 3;
-
- const xPos = (props.scale(item) + props.scale(props.data[endIndex]) + props.scale.bandwidth()) / 2;
-
- return ();
- })
- );
+
+ // Get offset in case of first period
+ if (props.visualizationPeriod !== "fiscal_year" && props.rawLabels) {
+ labelOffset = this.calculateDateOffset(props.rawLabels[0], props.visualizationPeriod);
}
- // Monthly View
+
return (
- props.data.map((item, index) => {
- // offset the D3 calculated position by the left padding and put the label in the middle
+ props.rawLabels.map((item, index) => {
+ // offset the D3 calculated position by the left padding and put the label in
+ // the middle
// of the each tick's width to center the text
- if (index % 12 !== 0) {
+ if ((index - labelOffset) % labelIterator !== 0 && index !== 0) {
return null;
}
- const endIndex = index + 11 > props.data.length ? props.data.length - 1 : index + 11;
-
- const xPos = (props.scale(item) + props.scale(props.data[endIndex]) + props.scale.bandwidth()) / 2;
-
- const label = (parseInt(item.split(" ")[1], 10) + 1).toString();
+ // Figure out what to call the label and where to place it
+ const label = this.calculateLabel(item, props);
+ const xPos = this.calculateXPos(item, index, labelOffset, props);
return ();
+ key={`label-x-${item}-${index}`} />);
})
);
}
+ // Finds the position of the label, under bar for years or
+ // average start and end for monthly/quartlery
+ calculateXPos(item, index, labelOffset, props) {
+ if (props.visualizationPeriod === 'fiscal_year') {
+ return props.scale(item.year) + (props.scale.bandwidth() / 2);
+ }
+ const endIndex = this.calculateEndIndex(
+ index,
+ props.data,
+ props.visualizationPeriod,
+ labelOffset);
+
+ // Need to use props.data because you cant scale by objects
+ return (props.scale(props.data[index]) + props.scale(props.data[endIndex]) + props.scale.bandwidth()) / 2;
+ }
+
+ // Gets the content of the label, year, break apart the quarter, or
+ // Fiscal year increments if the date range started between oct-dec
+ calculateLabel(item, props) {
+ if (props.visualizationPeriod === 'fiscal_year') {
+ return item.year;
+ }
+ const year = item.year;
+ if (props.visualizationPeriod === 'quarter') {
+ return year;
+ }
+ const months = ['Oct', 'Nov', 'Dec'];
+ const increment = months.indexOf(item.period) !== -1 ? 1 : 0;
+ return (parseInt(year, 10) + increment).toString();
+ }
+
+ // Calcuate how many periods until the end of the FY so that the year label
+ // is placed correctly
+ calculateDateOffset(item, type) {
+ const period = item.period;
+ if (type === 'month') {
+ // Fiscal year starts in October, so calculate how many months until the
+ // end of the year
+ // Mod 12 because 12 month offset == to 0 month offset
+ const months = ['Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May',
+ 'Jun', 'Jul', 'Aug', 'Sep'];
+ return (12 - months.indexOf(period)) % 12;
+ }
+ // Calculate how many quarters left in the year
+ // Mod 4 because 4 quarter offset == 0 quarter offset
+ const quarters = ['Q1', 'Q2', 'Q3', 'Q4'];
+ return (4 - quarters.indexOf(period)) % 4;
+ }
+
+ // Finds the end of the year for a range of dates
+ // Only matters for the first section and last since the date range can start
+ // in the middle or not be finished yet. Every other date should be a full range
+ calculateEndIndex(index, data, type, offset) {
+ // Blocks of 4 for quarters (0-3)
+ if (type === 'quarter') {
+ let endIndex = index + 3;
+ if (index < offset) {
+ endIndex = offset - 1;
+ }
+ if (endIndex >= data.length) {
+ endIndex = data.length - 1;
+ }
+ return endIndex;
+ }
+
+ // Blocks of 12 for monthly (0-11)
+ let endIndex = index + 11;
+ if (index < offset) {
+ endIndex = offset - 1;
+ }
+ if (endIndex >= data.length) {
+ endIndex = data.length - 1;
+ }
+ return endIndex;
+ }
+
drawAxis(props) {
if (!props.scale) {
return;
diff --git a/src/js/components/sharedComponents/LoadingSpinner.jsx b/src/js/components/sharedComponents/LoadingSpinner.jsx
new file mode 100644
index 0000000000..90e9dcd8fb
--- /dev/null
+++ b/src/js/components/sharedComponents/LoadingSpinner.jsx
@@ -0,0 +1,48 @@
+/**
+ * LoadingScreen.jsx
+ * Created by Kevin Li 12/20/17
+ */
+
+import React from 'react';
+
+const LoadingSpinner = () => (
+
+
+
+);
+
+export default LoadingSpinner;
diff --git a/src/js/components/sharedComponents/icons/Icons.jsx b/src/js/components/sharedComponents/icons/Icons.jsx
index b48d99f92a..f7c677dcc6 100644
--- a/src/js/components/sharedComponents/icons/Icons.jsx
+++ b/src/js/components/sharedComponents/icons/Icons.jsx
@@ -397,3 +397,9 @@ Recipient.defaultProps = {
alt: 'Icon Depicting a Person Representing Recipients'
};
+export class CircleArrowLeft extends BaseIcon {}
+CircleArrowLeft.defaultProps = {
+ iconName: 'usa-da-circle-arrow-left',
+ iconClass: 'usa-da-circle-arrow-left',
+ alt: 'Icon Depicting an Arrow in a Circle Pointing Left'
+};
diff --git a/src/js/containers/account/topFilterBar/AccountTopFilterBarContainer.jsx b/src/js/containers/account/topFilterBar/AccountTopFilterBarContainer.jsx
index 8cce818479..96de37450d 100644
--- a/src/js/containers/account/topFilterBar/AccountTopFilterBarContainer.jsx
+++ b/src/js/containers/account/topFilterBar/AccountTopFilterBarContainer.jsx
@@ -10,7 +10,7 @@ import { connect } from 'react-redux';
import { orderBy } from 'lodash';
import moment from 'moment';
-import TopFilterBar from 'components/search/topFilterBar/TopFilterBar';
+import LegacyTopFilterBar from 'components/account/topFilterBar/LegacyTopFilterBar';
import { topFilterGroupGenerator } from
'components/account/topFilterBar/filterGroups/AccountTopFilterGroupGenerator';
@@ -161,7 +161,7 @@ export class AccountTopFilterBarContainer extends React.Component {
count += filter.values.length;
});
- output = ( {
diff --git a/src/js/containers/bulkDownload/archive/AwardDataArchiveContainer.jsx b/src/js/containers/bulkDownload/archive/AwardDataArchiveContainer.jsx
index 5179c764e3..5f8b9ad9e3 100644
--- a/src/js/containers/bulkDownload/archive/AwardDataArchiveContainer.jsx
+++ b/src/js/containers/bulkDownload/archive/AwardDataArchiveContainer.jsx
@@ -17,7 +17,7 @@ const columns = [
displayName: 'Agency'
},
{
- columnName: 'url',
+ columnName: 'fileName',
displayName: 'Archive File'
},
{
@@ -155,7 +155,8 @@ export default class AwardDataArchiveContainer extends React.Component {
const file = {
agency: formattedAgency,
- url: item.file_name,
+ fileName: item.file_name,
+ url: item.url,
fy: formattedFY,
date: formattedDate
};
diff --git a/src/js/containers/bulkDownload/modal/BulkDownloadBottomBarContainer.jsx b/src/js/containers/bulkDownload/modal/BulkDownloadBottomBarContainer.jsx
index 63b98168cc..f027a30bfc 100644
--- a/src/js/containers/bulkDownload/modal/BulkDownloadBottomBarContainer.jsx
+++ b/src/js/containers/bulkDownload/modal/BulkDownloadBottomBarContainer.jsx
@@ -32,8 +32,8 @@ export class BulkDownloadBottomBarContainer extends React.Component {
visible: false,
showError: false,
showSuccess: false,
- title: 'Your file is being generated...',
- description: 'Warning: In order to complete your download, please remain on this site.'
+ title: 'We\'re preparing your download(s)...',
+ description: 'If you plan to leave the site, copy the download link before you go - you\'ll need it to access your file.'
};
this.request = null;
@@ -75,8 +75,8 @@ export class BulkDownloadBottomBarContainer extends React.Component {
visible: true,
showError: false,
showSuccess: false,
- title: 'Your file is being generated...',
- description: 'Warning: In order to complete your download, please remain on this site.'
+ title: 'We\'re preparing your download(s)...',
+ description: 'If you plan to leave the site, copy the download link before you go - you\'ll need it to access your file.'
}, this.checkStatus);
}
@@ -193,6 +193,7 @@ will no longer download to your computer. Are you sure you want to do this?`;
if (this.state.visible) {
content = ( {
this.request = null;
+ this.props.setAppliedFilterEmptiness(false);
this.applyFilters(res.data.filter);
})
.catch((err) => {
@@ -181,6 +194,8 @@ export class SearchContainer extends React.Component {
hash: '',
hashState: 'ready'
}, () => {
+ this.props.setAppliedFilterEmptiness(true);
+ this.props.setAppliedFilterCompletion(true);
Router.history.replace('/search');
});
}
@@ -214,6 +229,8 @@ export class SearchContainer extends React.Component {
});
this.props.populateAllSearchFilters(reduxValues);
+ // also overwrite the staged filters with the same values
+ this.props.applyStagedFilters(reduxValues);
this.setState({
hashState: 'ready'
@@ -265,13 +282,9 @@ export class SearchContainer extends React.Component {
const unfiltered = this.determineIfUnfiltered(filters);
if (unfiltered) {
// all the filters were cleared, reset to a blank hash
- this.setState({
- hash: '',
- hashState: 'ready'
- }, () => {
- Router.history.replace('/search');
- });
-
+ this.props.setAppliedFilterEmptiness(true);
+ this.props.setAppliedFilterCompletion(true);
+ Router.history.replace('/search');
return;
}
@@ -327,6 +340,14 @@ export class SearchContainer extends React.Component {
}
requestDownloadAvailability(filters) {
+ if (this.determineIfUnfiltered(filters)) {
+ // don't make an API call when it's a blank state
+ this.setState({
+ downloadAvailable: false
+ });
+ return;
+ }
+
const operation = new SearchAwardsOperation();
operation.fromState(filters);
const searchParams = operation.toParams();
@@ -364,20 +385,27 @@ export class SearchContainer extends React.Component {
return (
+ downloadAvailable={this.state.downloadAvailable}
+ download={this.props.download}
+ requestsComplete={this.props.appliedFilters._complete} />
);
}
}
export default connect(
(state) => ({
- filters: state.filters
+ filters: state.filters,
+ download: state.download,
+ appliedFilters: state.appliedFilters
}),
(dispatch) => bindActionCreators(Object.assign({}, searchHashActions, {
- clearAllFilters
+ clearAllFilters,
+ applyStagedFilters,
+ setAppliedFilterEmptiness,
+ setAppliedFilterCompletion
}), dispatch)
)(SearchContainer);
diff --git a/src/js/containers/search/SearchSidebarSubmitContainer.jsx b/src/js/containers/search/SearchSidebarSubmitContainer.jsx
new file mode 100644
index 0000000000..9124272aa9
--- /dev/null
+++ b/src/js/containers/search/SearchSidebarSubmitContainer.jsx
@@ -0,0 +1,124 @@
+/**
+ * SearchSidebarSubmitContainer.jsx
+ * Created by Kevin Li 12/21/17
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import { is } from 'immutable';
+
+import * as appliedFilterActions from 'redux/actions/search/appliedFilterActions';
+import { clearAllFilters as clearStagedFilters } from 'redux/actions/search/searchFilterActions';
+
+import SearchSidebarSubmit from 'components/search/SearchSidebarSubmit';
+
+const combinedActions = Object.assign({}, appliedFilterActions, {
+ clearStagedFilters
+});
+
+const propTypes = {
+ stagedFilters: PropTypes.object,
+ appliedFilters: PropTypes.object,
+ requestsComplete: PropTypes.bool,
+ applyStagedFilters: PropTypes.func,
+ clearStagedFilters: PropTypes.func,
+ setAppliedFilterCompletion: PropTypes.func,
+ resetAppliedFilters: PropTypes.func
+};
+
+export class SearchSidebarSubmitContainer extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ filtersChanged: false
+ };
+
+ this.resetFilters = this.resetFilters.bind(this);
+ this.applyStagedFilters = this.applyStagedFilters.bind(this);
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.stagedFilters !== this.props.stagedFilters) {
+ this.stagingChanged();
+ }
+ else if (prevProps.appliedFilters !== this.props.appliedFilters) {
+ this.stagingChanged();
+ }
+ }
+
+ compareStores() {
+ // we need to do a deep equality check by comparing every store key
+ const storeKeys = Object.keys(this.props.stagedFilters);
+ if (storeKeys.length !== Object.keys(this.props.appliedFilters).length) {
+ // key lengths do not match, there's a difference so fail immediately
+ return false;
+ }
+
+ for (const key of storeKeys) {
+ if (!{}.hasOwnProperty.call(this.props.appliedFilters, key)) {
+ // no such key, immediately fail
+ return false;
+ }
+
+ if (!is(this.props.appliedFilters[key], this.props.stagedFilters[key])) {
+ // use immutable to check equality of nested objects
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ stagingChanged() {
+ // do a deep equality check between the staged filters and applied filters
+ if (!this.compareStores()) {
+ this.setState({
+ filtersChanged: true
+ });
+ }
+ else if (this.state.filtersChanged) {
+ this.setState({
+ filtersChanged: false
+ });
+ }
+ }
+
+ applyStagedFilters() {
+ this.props.setAppliedFilterCompletion(false);
+ this.props.applyStagedFilters(this.props.stagedFilters);
+ this.setState({
+ filtersChanged: false
+ });
+ }
+
+ resetFilters() {
+ this.props.clearStagedFilters();
+ this.props.resetAppliedFilters();
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default connect(
+ (state) => ({
+ requestsComplete: state.appliedFilters._complete,
+ isEmpty: state.appliedFilters._empty,
+ stagedFilters: state.filters,
+ appliedFilters: state.appliedFilters.filters
+ }),
+ (dispatch) => bindActionCreators(combinedActions, dispatch)
+)(SearchSidebarSubmitContainer);
+
+SearchSidebarSubmitContainer.propTypes = propTypes;
diff --git a/src/js/containers/search/filters/KeywordContainer.jsx b/src/js/containers/search/filters/KeywordContainer.jsx
index 8d780af35f..daebd7c507 100644
--- a/src/js/containers/search/filters/KeywordContainer.jsx
+++ b/src/js/containers/search/filters/KeywordContainer.jsx
@@ -37,6 +37,7 @@ export class KeywordContainer extends React.Component {
this.submitText = this.submitText.bind(this);
this.changedInput = this.changedInput.bind(this);
+ this.removeKeyword = this.removeKeyword.bind(this);
}
componentWillMount() {
@@ -73,12 +74,22 @@ export class KeywordContainer extends React.Component {
}
}
+ removeKeyword() {
+ this.setState({
+ value: ''
+ }, () => {
+ this.submitText();
+ });
+ }
+
render() {
return (
+ submitText={this.submitText}
+ removeKeyword={this.removeKeyword} />
);
}
}
diff --git a/src/js/containers/search/filters/SearchSidebarContainer.jsx b/src/js/containers/search/filters/SearchSidebarContainer.jsx
new file mode 100644
index 0000000000..6323013320
--- /dev/null
+++ b/src/js/containers/search/filters/SearchSidebarContainer.jsx
@@ -0,0 +1,34 @@
+/**
+ * SearchSidebarContainer.jsx
+ * Created by Kevin Li 12/19/17
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import SearchSidebar from 'components/search/SearchSidebar';
+
+import * as searchFilterActions from 'redux/actions/search/searchFilterActions';
+
+const propTypes = {
+ filters: PropTypes.object
+};
+
+export class SearchSidebarContainer extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+}
+
+SearchSidebarContainer.propTypes = propTypes;
+
+export default connect(
+ (state) => ({
+ filters: state.filters
+ }),
+ (dispatch) => bindActionCreators(searchFilterActions, dispatch)
+)(SearchSidebarContainer);
diff --git a/src/js/containers/search/modals/fullDownload/DownloadBottomBarContainer.jsx b/src/js/containers/search/modals/fullDownload/DownloadBottomBarContainer.jsx
index 4b12a0da7a..8485ffe14e 100644
--- a/src/js/containers/search/modals/fullDownload/DownloadBottomBarContainer.jsx
+++ b/src/js/containers/search/modals/fullDownload/DownloadBottomBarContainer.jsx
@@ -22,6 +22,7 @@ const propTypes = {
setDownloadPending: PropTypes.func,
setDownloadCollapsed: PropTypes.func,
setDownloadExpectedFile: PropTypes.func,
+ setDownloadExpectedUrl: PropTypes.func,
resetDownload: PropTypes.func,
filters: PropTypes.object
};
@@ -34,8 +35,8 @@ export class DownloadBottomBarContainer extends React.Component {
visible: false,
showError: false,
showSuccess: false,
- title: 'Your file is being generated...',
- description: 'Warning: In order to complete your download, please remain on this site.'
+ title: 'We\'re preparing your download(s)...',
+ description: 'If you plan to leave the site, copy the download link before you go - you\'ll need it to access your file.'
};
this.request = null;
@@ -79,8 +80,8 @@ export class DownloadBottomBarContainer extends React.Component {
visible: true,
showError: false,
showSuccess: false,
- title: 'Your file is being generated...',
- description: 'Warning: In order to complete your download, please remain on this site.'
+ title: 'We\'re preparing your download(s)...',
+ description: 'If you plan to leave the site, copy the download link before you go - you\'ll need it to access your file.'
}, this.checkStatus);
}
@@ -108,6 +109,7 @@ export class DownloadBottomBarContainer extends React.Component {
this.request.promise
.then((res) => {
this.props.setDownloadExpectedFile(res.data.file_name);
+ this.props.setDownloadExpectedUrl(res.data.url);
this.checkStatus();
})
.catch((err) => {
diff --git a/src/js/containers/search/modals/fullDownload/FullDownloadModalContainer.jsx b/src/js/containers/search/modals/fullDownload/FullDownloadModalContainer.jsx
index b65559d262..c81bf982ca 100644
--- a/src/js/containers/search/modals/fullDownload/FullDownloadModalContainer.jsx
+++ b/src/js/containers/search/modals/fullDownload/FullDownloadModalContainer.jsx
@@ -16,7 +16,8 @@ const propTypes = {
mounted: PropTypes.bool,
hideModal: PropTypes.func,
setDownloadCollapsed: PropTypes.func,
- pendingDownload: PropTypes.bool
+ pendingDownload: PropTypes.bool,
+ download: PropTypes.object
};
export class FullDownloadModalContainer extends React.Component {
@@ -25,6 +26,7 @@ export class FullDownloadModalContainer extends React.Component {
);
@@ -34,6 +36,9 @@ export class FullDownloadModalContainer extends React.Component {
FullDownloadModalContainer.propTypes = propTypes;
export default connect(
- (state) => ({ pendingDownload: state.download.pendingDownload }),
+ (state) => ({
+ pendingDownload: state.download.pendingDownload,
+ download: state.download
+ }),
(dispatch) => bindActionCreators(downloadActions, dispatch)
)(FullDownloadModalContainer);
diff --git a/src/js/containers/search/table/ResultsTableContainer.jsx b/src/js/containers/search/table/ResultsTableContainer.jsx
index c2fac545e4..b727306dd7 100644
--- a/src/js/containers/search/table/ResultsTableContainer.jsx
+++ b/src/js/containers/search/table/ResultsTableContainer.jsx
@@ -23,13 +23,16 @@ import { measureTableHeader } from 'helpers/textMeasurement';
import ResultsTableSection from 'components/search/table/ResultsTableSection';
import SearchActions from 'redux/actions/searchActions';
+import * as appliedFilterActions from 'redux/actions/search/appliedFilterActions';
const propTypes = {
filters: PropTypes.object,
columnVisibility: PropTypes.object,
toggleColumnVisibility: PropTypes.func,
reorderColumns: PropTypes.func,
- populateAvailableColumns: PropTypes.func
+ populateAvailableColumns: PropTypes.func,
+ setAppliedFilterCompletion: PropTypes.func,
+ noApplied: PropTypes.bool
};
const tableTypes = [
@@ -76,6 +79,7 @@ export class ResultsTableContainer extends React.Component {
direction: 'desc'
},
inFlight: true,
+ error: false,
results: [],
tableInstance: `${uniqueId()}` // this will stay constant during pagination but will change when the filters or table type changes
};
@@ -99,7 +103,7 @@ export class ResultsTableContainer extends React.Component {
}
componentDidUpdate(prevProps) {
- if (prevProps.filters !== this.props.filters) {
+ if (prevProps.filters !== this.props.filters && !this.props.noApplied) {
// filters changed, update the search object
this.pickDefaultTab();
}
@@ -123,8 +127,11 @@ export class ResultsTableContainer extends React.Component {
this.tabCountRequest.cancel();
}
+ this.props.setAppliedFilterCompletion(false);
+
this.setState({
- inFlight: true
+ inFlight: true,
+ error: false
});
const searchParams = new SearchAwardsOperation();
@@ -140,7 +147,15 @@ export class ResultsTableContainer extends React.Component {
this.parseTabCounts(res.data);
})
.catch((err) => {
- console.log(err);
+ if (!isCancel(err)) {
+ this.setState({
+ inFlight: false,
+ error: true
+ });
+ this.props.setAppliedFilterCompletion(true);
+
+ console.log(err);
+ }
});
}
@@ -240,6 +255,8 @@ export class ResultsTableContainer extends React.Component {
this.searchRequest.cancel();
}
+ this.props.setAppliedFilterCompletion(false);
+
const tableType = this.state.tableType;
// Append the current tab's award types to the search params if the Award Type filter
@@ -263,7 +280,8 @@ export class ResultsTableContainer extends React.Component {
// indicate the request is about to start
this.setState({
- inFlight: true
+ inFlight: true,
+ error: false
});
let pageNumber = this.state.page;
@@ -325,20 +343,18 @@ export class ResultsTableContainer extends React.Component {
newState.lastPage = !res.data.page_metadata.hasNext;
this.setState(newState);
+
+ this.props.setAppliedFilterCompletion(true);
})
.catch((err) => {
- if (isCancel(err)) {
- // the request was cancelled
- }
- else if (err.response) {
- // server responded with something
- console.log(err);
- this.searchRequest = null;
- }
- else {
- // request never made it out
+ if (!isCancel(err)) {
+ this.setState({
+ inFlight: false,
+ error: true
+ });
+ this.props.setAppliedFilterCompletion(true);
+
console.log(err);
- this.searchRequest = null;
}
});
}
@@ -422,6 +438,7 @@ export class ResultsTableContainer extends React.Component {
const tableType = this.state.tableType;
return (
({
- filters: state.filters,
+ filters: state.appliedFilters.filters,
+ noApplied: state.appliedFilters._empty,
columnVisibility: state.columnVisibility
}),
- (dispatch) => bindActionCreators(SearchActions, dispatch)
+ (dispatch) => bindActionCreators(Object.assign({}, SearchActions, appliedFilterActions), dispatch)
)(ResultsTableContainer);
diff --git a/src/js/containers/search/topFilterBar/TopFilterBarContainer.jsx b/src/js/containers/search/topFilterBar/TopFilterBarContainer.jsx
index bc7015b314..8ecc1080da 100644
--- a/src/js/containers/search/topFilterBar/TopFilterBarContainer.jsx
+++ b/src/js/containers/search/topFilterBar/TopFilterBarContainer.jsx
@@ -290,7 +290,7 @@ export class TopFilterBarContainer extends React.Component {
if (selected) {
filter.code = 'selectedLocations';
- filter.name = 'Place of Performance Location';
+ filter.name = 'Place of Performance';
return filter;
}
return null;
@@ -639,6 +639,6 @@ TopFilterBarContainer.propTypes = propTypes;
TopFilterBarContainer.defaultProps = defaultProps;
export default connect(
- (state) => ({ reduxFilters: state.filters }),
+ (state) => ({ reduxFilters: state.appliedFilters.filters }),
(dispatch) => bindActionCreators(searchFilterActions, dispatch)
)(TopFilterBarContainer);
diff --git a/src/js/containers/search/visualizations/geo/GeoVisualizationSectionContainer.jsx b/src/js/containers/search/visualizations/geo/GeoVisualizationSectionContainer.jsx
index 52a2c3affa..b3172ff3f0 100644
--- a/src/js/containers/search/visualizations/geo/GeoVisualizationSectionContainer.jsx
+++ b/src/js/containers/search/visualizations/geo/GeoVisualizationSectionContainer.jsx
@@ -14,6 +14,7 @@ import GeoVisualizationSection from
'components/search/visualizations/geo/GeoVisualizationSection';
import * as searchFilterActions from 'redux/actions/search/searchFilterActions';
+import { setAppliedFilterCompletion } from 'redux/actions/search/appliedFilterActions';
import * as SearchHelper from 'helpers/searchHelper';
import MapBroadcaster from 'helpers/mapBroadcaster';
@@ -22,7 +23,9 @@ import SearchAwardsOperation from 'models/search/SearchAwardsOperation';
const propTypes = {
reduxFilters: PropTypes.object,
- resultsMeta: PropTypes.object
+ resultsMeta: PropTypes.object,
+ setAppliedFilterCompletion: PropTypes.func,
+ noApplied: PropTypes.bool
};
const apiScopes = {
@@ -46,7 +49,7 @@ export class GeoVisualizationSectionContainer extends React.Component {
renderHash: `geo-${uniqueId()}`,
loading: true,
loadingTiles: true,
- message: ''
+ error: false
};
this.apiRequest = null;
@@ -70,7 +73,7 @@ export class GeoVisualizationSectionContainer extends React.Component {
}
componentDidUpdate(prevProps) {
- if (!isEqual(prevProps.reduxFilters, this.props.reduxFilters)) {
+ if (!isEqual(prevProps.reduxFilters, this.props.reduxFilters) && !this.props.noApplied) {
this.prepareFetch(true);
}
}
@@ -162,6 +165,19 @@ export class GeoVisualizationSectionContainer extends React.Component {
const operation = new SearchAwardsOperation();
operation.fromState(this.props.reduxFilters);
+ // if no entities are visible, don't make an API rquest because nothing in the US is visible
+ if (this.state.visibleEntities.length === 0) {
+ this.setState({
+ loading: false,
+ error: false,
+ data: {
+ values: [],
+ locations: []
+ }
+ });
+ return;
+ }
+
const searchParams = operation.toParams();
// generate the API parameters
@@ -179,9 +195,11 @@ export class GeoVisualizationSectionContainer extends React.Component {
this.setState({
loading: true,
- message: 'Loading data...'
+ error: false
});
+ this.props.setAppliedFilterCompletion(false);
+
this.apiRequest = SearchHelper.performSpendingByGeographySearch(apiParams);
this.apiRequest.promise
.then((res) => {
@@ -195,8 +213,10 @@ export class GeoVisualizationSectionContainer extends React.Component {
this.setState({
loading: false,
- message: 'An error occurred while loading map data.'
+ error: true
});
+
+ this.props.setAppliedFilterCompletion(true);
}
});
}
@@ -218,20 +238,17 @@ export class GeoVisualizationSectionContainer extends React.Component {
}
});
- let message = '';
- if (data.results.length === 0) {
- message = 'No results in the current map area.';
- }
+ this.props.setAppliedFilterCompletion(true);
this.setState({
- message,
data: {
values: spendingValues,
locations: spendingShapes,
labels: spendingLabels
},
renderHash: `geo-${uniqueId()}`,
- loading: false
+ loading: false,
+ error: false
});
}
@@ -249,6 +266,7 @@ export class GeoVisualizationSectionContainer extends React.Component {
return (
);
@@ -258,6 +276,11 @@ export class GeoVisualizationSectionContainer extends React.Component {
GeoVisualizationSectionContainer.propTypes = propTypes;
export default connect(
- (state) => ({ reduxFilters: state.filters }),
- (dispatch) => bindActionCreators(searchFilterActions, dispatch)
+ (state) => ({
+ reduxFilters: state.appliedFilters.filters,
+ noApplied: state.appliedFilters._empty
+ }),
+ (dispatch) => bindActionCreators(Object.assign({}, searchFilterActions, {
+ setAppliedFilterCompletion
+ }), dispatch)
)(GeoVisualizationSectionContainer);
diff --git a/src/js/containers/search/visualizations/time/TimeVisualizationSectionContainer.jsx b/src/js/containers/search/visualizations/time/TimeVisualizationSectionContainer.jsx
index 3d4bc1c759..f9daa3914e 100644
--- a/src/js/containers/search/visualizations/time/TimeVisualizationSectionContainer.jsx
+++ b/src/js/containers/search/visualizations/time/TimeVisualizationSectionContainer.jsx
@@ -8,22 +8,27 @@ import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { isEqual } from 'lodash';
+import { isCancel } from 'axios';
import TimeVisualizationSection from
'components/search/visualizations/time/TimeVisualizationSection';
import * as searchFilterActions from 'redux/actions/search/searchFilterActions';
+import { setAppliedFilterCompletion } from 'redux/actions/search/appliedFilterActions';
import * as SearchHelper from 'helpers/searchHelper';
import * as MonthHelper from 'helpers/monthHelper';
import SearchAwardsOperation from 'models/search/SearchAwardsOperation';
-const combinedActions = Object.assign({}, searchFilterActions);
+const combinedActions = Object.assign({}, searchFilterActions, {
+ setAppliedFilterCompletion
+});
const propTypes = {
reduxFilters: PropTypes.object,
- setVizTxnSum: PropTypes.func
+ setAppliedFilterCompletion: PropTypes.func,
+ noApplied: PropTypes.bool
};
export class TimeVisualizationSectionContainer extends React.Component {
@@ -33,6 +38,7 @@ export class TimeVisualizationSectionContainer extends React.Component {
this.state = {
visualizationPeriod: 'fiscal_year',
loading: true,
+ error: false,
groups: [],
xSeries: [],
ySeries: []
@@ -47,7 +53,7 @@ export class TimeVisualizationSectionContainer extends React.Component {
}
componentDidUpdate(prevProps) {
- if (!isEqual(prevProps.reduxFilters, this.props.reduxFilters)) {
+ if (!isEqual(prevProps.reduxFilters, this.props.reduxFilters) && !this.props.noApplied) {
this.fetchData();
}
}
@@ -61,8 +67,10 @@ export class TimeVisualizationSectionContainer extends React.Component {
}
fetchData() {
+ this.props.setAppliedFilterCompletion(false);
this.setState({
- loading: true
+ loading: true,
+ error: false
});
// Cancel API request if it exists
@@ -96,8 +104,18 @@ export class TimeVisualizationSectionContainer extends React.Component {
this.parseData(res.data, this.state.visualizationPeriod);
this.apiRequest = null;
})
- .catch(() => {
+ .catch((err) => {
+ if (isCancel(err)) {
+ return;
+ }
+
+ this.props.setAppliedFilterCompletion(true);
this.apiRequest = null;
+ console.log(err);
+ this.setState({
+ loading: false,
+ error: true
+ });
});
}
@@ -115,31 +133,52 @@ export class TimeVisualizationSectionContainer extends React.Component {
return `${month} ${year}`;
}
+ generateTimeRaw(group, timePeriod) {
+ if (group === 'fiscal_year') {
+ return {
+ period: null,
+ year: timePeriod.fiscal_year
+ };
+ }
+ else if (group === 'quarter') {
+ return {
+ period: `Q${timePeriod.quarter}`,
+ year: `${timePeriod.fiscal_year}`
+ };
+ }
+
+ const month = MonthHelper.convertNumToShortMonth(timePeriod.month);
+ const year = MonthHelper.convertMonthToFY(timePeriod.month, timePeriod.fiscal_year);
+
+ return {
+ period: `${month}`,
+ year: `${year}`
+ };
+ }
+
parseData(data, group) {
const groups = [];
const xSeries = [];
const ySeries = [];
-
- let totalSpending = 0;
+ const rawLabels = [];
// iterate through each response object and break it up into groups, x series, and y series
data.results.forEach((item) => {
groups.push(this.generateTimeLabel(group, item.time_period));
+ rawLabels.push(this.generateTimeRaw(group, item.time_period));
xSeries.push([this.generateTimeLabel(group, item.time_period)]);
ySeries.push([parseFloat(item.aggregated_amount)]);
-
- totalSpending += parseFloat(item.aggregated_amount);
});
this.setState({
groups,
xSeries,
ySeries,
- loading: false
+ rawLabels,
+ loading: false,
+ error: false
}, () => {
- // save the total spending amount to Redux so all visualizations have access to this
- // data
- this.props.setVizTxnSum(totalSpending);
+ this.props.setAppliedFilterCompletion(true);
});
}
@@ -147,7 +186,8 @@ export class TimeVisualizationSectionContainer extends React.Component {
return (
+ updateVisualizationPeriod={this.updateVisualizationPeriod}
+ visualizationPeriod={this.state.visualizationPeriod} />
);
}
}
@@ -155,6 +195,9 @@ export class TimeVisualizationSectionContainer extends React.Component {
TimeVisualizationSectionContainer.propTypes = propTypes;
export default connect(
- (state) => ({ reduxFilters: state.filters }),
+ (state) => ({
+ reduxFilters: state.appliedFilters.filters,
+ noApplied: state.appliedFilters._empty
+ }),
(dispatch) => bindActionCreators(combinedActions, dispatch)
)(TimeVisualizationSectionContainer);
diff --git a/src/js/models/search/SearchAwardsOperation.js b/src/js/models/search/SearchAwardsOperation.js
index e1b9ed5172..305105987d 100644
--- a/src/js/models/search/SearchAwardsOperation.js
+++ b/src/js/models/search/SearchAwardsOperation.js
@@ -177,17 +177,20 @@ class SearchAwardsOperation {
filters[rootKeys.recipients] = this.selectedRecipients;
}
- if (this.recipientDomesticForeign !== '' && this.recipientDomesticForeign !== 'all') {
- filters[rootKeys.recipientLocationScope] = this.recipientDomesticForeign;
- }
-
if (this.selectedRecipientLocations.length > 0) {
const locationSet = [];
this.selectedRecipientLocations.forEach((location) => {
- locationSet.push(location.filter);
+ if (location.filter.country && location.filter.country === 'FOREIGN') {
+ filters[rootKeys.recipientLocationScope] = 'foreign';
+ }
+ else {
+ locationSet.push(location.filter);
+ }
});
- filters[rootKeys.recipientLocation] = locationSet;
+ if (locationSet.length > 0) {
+ filters[rootKeys.recipientLocation] = locationSet;
+ }
}
if (this.recipientType.length > 0) {
@@ -198,14 +201,17 @@ class SearchAwardsOperation {
if (this.selectedLocations.length > 0) {
const locationSet = [];
this.selectedLocations.forEach((location) => {
- locationSet.push(location.filter);
+ if (location.filter.country && location.filter.country === 'FOREIGN') {
+ filters[rootKeys.placeOfPerformanceScope] = 'foreign';
+ }
+ else {
+ locationSet.push(location.filter);
+ }
});
- filters[rootKeys.placeOfPerformance] = locationSet;
- }
-
- if (this.locationDomesticForeign !== '' && this.locationDomesticForeign !== 'all') {
- filters[rootKeys.placeOfPerformanceScope] = this.locationDomesticForeign;
+ if (locationSet.length > 0) {
+ filters[rootKeys.placeOfPerformance] = locationSet;
+ }
}
// Add Award Amounts
diff --git a/src/js/redux/actions/search/appliedFilterActions.js b/src/js/redux/actions/search/appliedFilterActions.js
new file mode 100644
index 0000000000..5ca67ce8d7
--- /dev/null
+++ b/src/js/redux/actions/search/appliedFilterActions.js
@@ -0,0 +1,23 @@
+/**
+ * appliedFilterActions.js
+ * Created by Kevin Li 12/21/17
+ */
+
+export const setAppliedFilterCompletion = (complete) => ({
+ complete,
+ type: 'SET_APPLIED_FILTER_COMPLETION'
+});
+
+export const setAppliedFilterEmptiness = (empty) => ({
+ empty,
+ type: 'SET_APPLIED_FILTER_EMPTINESS'
+});
+
+export const applyStagedFilters = (filters) => ({
+ filters,
+ type: 'APPLY_STAGED_FILTERS'
+});
+
+export const resetAppliedFilters = () => ({
+ type: 'CLEAR_APPLIED_FILTERS'
+});
diff --git a/src/js/redux/actions/search/downloadActions.js b/src/js/redux/actions/search/downloadActions.js
index 741718be11..53e7a196d3 100644
--- a/src/js/redux/actions/search/downloadActions.js
+++ b/src/js/redux/actions/search/downloadActions.js
@@ -18,6 +18,11 @@ export const setDownloadExpectedFile = (state) => ({
file: state
});
+export const setDownloadExpectedUrl = (state) => ({
+ type: 'SET_DOWNLOAD_EXPECTED_URL',
+ url: state
+});
+
export const setDownloadPending = (state) => ({
state,
type: 'SET_DOWNLOAD_PENDING'
diff --git a/src/js/redux/reducers/index.js b/src/js/redux/reducers/index.js
index 537d62ba32..ae587715f9 100644
--- a/src/js/redux/reducers/index.js
+++ b/src/js/redux/reducers/index.js
@@ -6,6 +6,7 @@
import { combineReducers } from 'redux';
import filtersReducer from './search/searchFiltersReducer';
+import appliedFiltersReducer from './search/appliedFiltersReducer';
import columnVisibilityReducer from './search/columnVisibilityReducer';
import awardReducer from './award/awardReducer';
import accountReducer from './account/accountReducer';
@@ -18,6 +19,7 @@ import bulkDownloadReducer from './bulkDownload/bulkDownloadReducer';
const appReducer = combineReducers({
filters: filtersReducer,
+ appliedFilters: appliedFiltersReducer,
columnVisibility: columnVisibilityReducer,
download: downloadReducer,
award: awardReducer,
diff --git a/src/js/redux/reducers/search/appliedFiltersReducer.js b/src/js/redux/reducers/search/appliedFiltersReducer.js
new file mode 100644
index 0000000000..5bd346e36e
--- /dev/null
+++ b/src/js/redux/reducers/search/appliedFiltersReducer.js
@@ -0,0 +1,35 @@
+/**
+ * appliedFiltersReducer.js
+ * Created by Kevin Li 12/20/17
+ */
+
+import { initialState as defaultFilters } from './searchFiltersReducer';
+
+export const initialState = {
+ filters: defaultFilters,
+ _empty: true,
+ _complete: true
+};
+
+const appliedFiltersReducer = (state = initialState, action) => {
+ switch (action.type) {
+ case 'APPLY_STAGED_FILTERS':
+ return Object.assign({}, state, {
+ filters: action.filters
+ });
+ case 'CLEAR_APPLIED_FILTERS':
+ return Object.assign({}, initialState);
+ case 'SET_APPLIED_FILTER_EMPTINESS':
+ return Object.assign({}, state, {
+ _empty: action.empty
+ });
+ case 'SET_APPLIED_FILTER_COMPLETION':
+ return Object.assign({}, state, {
+ _complete: action.complete
+ });
+ default:
+ return state;
+ }
+};
+
+export default appliedFiltersReducer;
diff --git a/src/js/redux/reducers/search/downloadReducer.js b/src/js/redux/reducers/search/downloadReducer.js
index 79b7582d2f..367d08e8c8 100644
--- a/src/js/redux/reducers/search/downloadReducer.js
+++ b/src/js/redux/reducers/search/downloadReducer.js
@@ -9,6 +9,7 @@ export const initialState = {
type: 'award',
columns: new List(),
expectedFile: '',
+ expectedUrl: '',
pendingDownload: false,
showCollapsedProgress: false
};
@@ -30,6 +31,11 @@ const downloadReducer = (state = initialState, action) => {
expectedFile: action.file
});
}
+ case 'SET_DOWNLOAD_EXPECTED_URL': {
+ return Object.assign({}, state, {
+ expectedUrl: action.url
+ });
+ }
case 'SET_DOWNLOAD_PENDING': {
return Object.assign({}, state, {
pendingDownload: action.state
diff --git a/src/js/redux/reducers/search/searchFiltersReducer.js b/src/js/redux/reducers/search/searchFiltersReducer.js
index 1bdcb1a282..e33f6dd968 100644
--- a/src/js/redux/reducers/search/searchFiltersReducer.js
+++ b/src/js/redux/reducers/search/searchFiltersReducer.js
@@ -11,7 +11,6 @@ import * as AgencyFilterFunctions from './filters/agencyFilterFunctions';
import * as RecipientFilterFunctions from './filters/recipientFilterFunctions';
import * as AwardAmountFilterFunctions from './filters/awardAmountFilterFunctions';
import * as OtherFilterFunctions from './filters/OtherFilterFunctions';
-import * as FiscalYearHelper from '../../../helpers/fiscalYearHelper';
import * as ContractFilterFunctions from './filters/contractFilterFunctions';
// update this version when changes to the reducer structure are made
@@ -41,7 +40,7 @@ export const requiredTypes = {
export const initialState = {
keyword: '',
timePeriodType: 'fy',
- timePeriodFY: new Set([`${FiscalYearHelper.currentFiscalYear()}`]),
+ timePeriodFY: new Set(),
timePeriodStart: null,
timePeriodEnd: null,
selectedLocations: new OrderedMap(),
diff --git a/tests/containers/account/topFilterBar/AccountTopFilterBarContainer-test.jsx b/tests/containers/account/topFilterBar/AccountTopFilterBarContainer-test.jsx
index 12bf0ab25d..8737359814 100644
--- a/tests/containers/account/topFilterBar/AccountTopFilterBarContainer-test.jsx
+++ b/tests/containers/account/topFilterBar/AccountTopFilterBarContainer-test.jsx
@@ -16,7 +16,7 @@ import { defaultFilters } from '../defaultFilters';
const prepareFiltersSpy = sinon.spy(AccountTopFilterBarContainer.prototype, 'prepareFilters');
// mock the child component by replacing it with a function that returns a null element
-jest.mock('components/search/topFilterBar/TopFilterBar', () =>
+jest.mock('components/account/topFilterBar/LegacyTopFilterBar', () =>
jest.fn(() => null));
describe('AccountTopFilterBarContainer', () => {
diff --git a/tests/containers/bulkDownload/archive/AwardDataArchiveContainer-test.jsx b/tests/containers/bulkDownload/archive/AwardDataArchiveContainer-test.jsx
index 186144bc23..b594989177 100644
--- a/tests/containers/bulkDownload/archive/AwardDataArchiveContainer-test.jsx
+++ b/tests/containers/bulkDownload/archive/AwardDataArchiveContainer-test.jsx
@@ -83,13 +83,15 @@ describe('AwardDataArchiveContainer', () => {
const formattedResults = [
{
agency: "Mock Agency 1 (ABC)",
- url: "mockFile1.zip",
+ fileName: "mockFile1.zip",
+ url: "http://mockFile_full.zip",
fy: "FY 1988",
date: "12/12/1987"
},
{
agency: "Mock Agency 2 (DEF)",
- url: "mockFile2.zip",
+ fileName: "mockFile2.zip",
+ url: "http://mockFile_delta.zip",
fy: "FY 1988",
date: "12/18/1987"
}
diff --git a/tests/containers/bulkDownload/mockData.js b/tests/containers/bulkDownload/mockData.js
index ed73c01fb0..e7b618346c 100644
--- a/tests/containers/bulkDownload/mockData.js
+++ b/tests/containers/bulkDownload/mockData.js
@@ -40,12 +40,10 @@ export const mockAgencies = {
export const mockSubAgencies = [
{
- subtier_agency_name: "Subtier Agency 1",
- subtier_agency_id: 5
+ subtier_agency_name: "Subtier Agency 1"
},
{
- subtier_agency_name: "Subtier Agency 2",
- subtier_agency_id: 6
+ subtier_agency_name: "Subtier Agency 2"
}
];
@@ -69,7 +67,6 @@ export const mockRedux = {
name: 'Mock Agency'
},
subAgency: {
- id: '456',
name: 'Mock Sub-Agency'
},
dateType: 'action_date',
diff --git a/tests/containers/search/SearchContainer-test.jsx b/tests/containers/search/SearchContainer-test.jsx
index 283ada76e3..5bed7a43ba 100644
--- a/tests/containers/search/SearchContainer-test.jsx
+++ b/tests/containers/search/SearchContainer-test.jsx
@@ -5,23 +5,20 @@
import React from 'react';
import { shallow } from 'enzyme';
-import sinon from 'sinon';
import { Set } from 'immutable';
import { SearchContainer } from 'containers/search/SearchContainer';
import * as SearchHelper from 'helpers/searchHelper';
import { initialState } from 'redux/reducers/search/searchFiltersReducer';
-import Router from 'containers/router/Router';
+import { initialState as initialApplied } from 'redux/reducers/search/appliedFiltersReducer';
import { mockHash, mockFilters, mockRedux, mockActions } from './mockSearchHashes';
+import Router from './mockRouter';
// force Jest to use native Node promises
// see: https://facebook.github.io/jest/docs/troubleshooting.html#unresolved-promises
global.Promise = require.requireActual('promise');
-// spy on specific functions inside the component
-const routerReplaceSpy = sinon.spy(Router.history, 'replace');
-
// mock the child component by replacing it with a function that returns a null element
jest.mock('components/search/SearchPage', () =>
jest.fn(() => null));
@@ -29,10 +26,10 @@ jest.mock('components/search/SearchPage', () =>
jest.mock('helpers/searchHelper', () => require('./filters/searchHelper'));
jest.mock('helpers/fiscalYearHelper', () => require('./filters/fiscalYearHelper'));
jest.mock('helpers/downloadHelper', () => require('./modals/fullDownload/downloadHelper'));
+jest.mock('containers/router/Router', () => require('./mockRouter'));
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
-
describe('SearchContainer', () => {
it('should try to resolve the current URL hash on mount', () => {
const container = shallow( {
const receiveHash = jest.fn();
container.instance().receiveHash = receiveHash;
- container.instance().componentWillReceiveProps(Object.assign({}, container.props(), {
+ container.instance().componentWillReceiveProps(Object.assign({}, mockActions, mockRedux, {
params: {
hash: '11111'
}
@@ -104,7 +101,9 @@ describe('SearchContainer', () => {
});
const nextProps = Object.assign({}, mockRedux, mockActions, {
- filters: nextFilters
+ appliedFilters: Object.assign({}, initialApplied, {
+ filters: nextFilters
+ })
});
const generateHash = jest.fn();
@@ -151,11 +150,8 @@ describe('SearchContainer', () => {
const generateHash = jest.fn();
container.instance().generateHash = generateHash;
- routerReplaceSpy.reset();
container.instance().generateInitialHash();
- expect(routerReplaceSpy.callCount).toEqual(1);
- expect(routerReplaceSpy.calledWith('/search')).toBeTruthy();
- routerReplaceSpy.reset();
+ expect(Router.history.replace).toHaveBeenLastCalledWith('/search');
expect(generateHash).toHaveBeenCalledTimes(0);
});
@@ -166,7 +162,9 @@ describe('SearchContainer', () => {
});
const redux = Object.assign({}, mockRedux, {
- filters
+ appliedFilters: {
+ filters
+ }
});
const container = shallow( {
{...mockActions}
{...mockRedux} />);
- routerReplaceSpy.reset();
container.instance().provideHash('12345');
- expect(routerReplaceSpy.callCount).toEqual(1);
- expect(routerReplaceSpy.calledWith('/search/12345')).toBeTruthy();
- routerReplaceSpy.reset();
+ expect(Router.history.replace).toHaveBeenLastCalledWith('/search/12345');
expect(container.state().hash).toEqual('12345');
});
@@ -249,9 +244,9 @@ describe('SearchContainer', () => {
it('should trigger a Redux action to apply the filters', () => {
const populateAction = jest.fn();
- const actions = {
+ const actions = Object.assign({}, mockActions, {
populateAllSearchFilters: populateAction
- };
+ });
const container = shallow( {
});
describe('requestDownloadAvailability', () => {
+ it('should not make an API requets if the applied filter state equals the initial blank filter state', () => {
+ const blankFilters = Object.assign({}, initialState);
+ const container = shallow();
+
+ const mockParse = jest.fn();
+ container.instance().parseDownloadAvailability = mockParse;
+
+ container.setState({
+ downloadAvailable: true
+ });
+
+ container.instance().requestDownloadAvailability(blankFilters);
+
+ expect(mockParse).toHaveBeenCalledTimes(0);
+ expect(container.state().downloadAvailable).toBeFalsy();
+
+ });
it('should make an API request for how many transaction rows will be returned', async () => {
+ const newFilters = Object.assign({}, initialState, {
+ timePeriodFY: new Set(['1990'])
+ });
+
const container = shallow();
@@ -315,7 +333,7 @@ describe('SearchContainer', () => {
const mockParse = jest.fn();
container.instance().parseDownloadAvailability = mockParse;
- container.instance().requestDownloadAvailability(mockRedux.filters);
+ container.instance().requestDownloadAvailability(newFilters);
await container.instance().downloadRequest.promise;
expect(mockParse).toHaveBeenCalledWith({
diff --git a/tests/containers/search/SearchSidebarSubmitContainer-test.jsx b/tests/containers/search/SearchSidebarSubmitContainer-test.jsx
new file mode 100644
index 0000000000..57ac5b67b8
--- /dev/null
+++ b/tests/containers/search/SearchSidebarSubmitContainer-test.jsx
@@ -0,0 +1,169 @@
+/**
+ * SearchSidebarSubmitContainer-test.jsx
+ * Created by Kevin Li 12/28/17
+ */
+
+import React from 'react';
+import { mount, shallow } from 'enzyme';
+import { Set } from 'immutable';
+
+import { initialState as initialApplied } from 'redux/reducers/search/appliedFiltersReducer'
+import { initialState as initialStaged } from 'redux/reducers/search/searchFiltersReducer'
+
+import { SearchSidebarSubmitContainer } from 'containers/search/SearchSidebarSubmitContainer';
+
+import { mockActions, mockRedux } from './mockSubmit';
+
+// mock the child component by replacing it with a function that returns a null element
+jest.mock('components/search/SearchSidebarSubmit', () =>
+ jest.fn(() => null));
+
+describe('SearchSidebarSubmitContainer', () => {
+ describe('compareStores', () => {
+ it('should return false if the length of enumerable properties on the applied filter object is different from the length of enumerable properties on the staged filter object', () => {
+ const changedStage = Object.assign({}, initialStaged, {
+ bonusFilter: 'hello'
+ });
+
+ const redux = Object.assign({}, mockRedux, {
+ stagedFilters: Object.assign({}, mockRedux.stagedFilters, changedStage)
+ });
+
+ const container = shallow(
+
+ );
+ const compare = container.instance().compareStores();
+ expect(compare).toBeFalsy();
+ });
+
+ it('should return false if any item in the staged filter object does not equal the same key value in the applied filter object', () => {
+ const changedStage = Object.assign({}, initialStaged, {
+ timePeriodFY: new Set(['1995'])
+ });
+
+ const redux = Object.assign({}, mockRedux, {
+ stagedFilters: Object.assign({}, mockRedux.stagedFilters, changedStage)
+ });
+
+ const container = shallow(
+
+ );
+ const compare = container.instance().compareStores();
+ expect(compare).toBeFalsy();
+ });
+
+ it('should return true if all key values are equal in both the staged and applied filter objects', () => {
+ const changedStage = Object.assign({}, initialStaged, {
+ timePeriodFY: new Set(['1995'])
+ });
+ const changedApplied = Object.assign({}, initialApplied.filters, {
+ timePeriodFY: new Set(['1995'])
+ });
+
+ const redux = Object.assign({}, mockRedux, {
+ stagedFilters: Object.assign({}, mockRedux.stagedFilters, changedStage),
+ appliedFilters: Object.assign({}, mockRedux.appliedFilters, changedApplied),
+ });
+
+ const container = shallow(
+
+ );
+ const compare = container.instance().compareStores();
+ expect(compare).toBeTruthy();
+ });
+ });
+ describe('stagingChanged', () => {
+ it('should set the filtersChanged state to true when the stores are not equal', () => {
+ const container = shallow(
+
+ );
+ container.instance().compareStores = jest.fn(() => false);
+
+ container.instance().stagingChanged();
+ expect(container.state().filtersChanged).toBeTruthy();
+ });
+ it('should set the filtersChanged state to false when the stores are equal and the filtersChanged state was previously true', () => {
+ const container = shallow(
+
+ );
+ container.instance().compareStores = jest.fn(() => true);
+ container.setState({
+ filtersChanged: true
+ });
+
+ container.instance().stagingChanged();
+ expect(container.state().filtersChanged).toBeFalsy();
+ });
+ });
+ describe('applyStagedFilters', () => {
+ it('should tell Redux to copy the staged filter set to the applied filter set', () => {
+ const actions = Object.assign({}, mockActions, {
+ applyStagedFilters: jest.fn()
+ });
+
+ const container = shallow(
+
+ );
+ container.instance().applyStagedFilters();
+
+ expect(actions.applyStagedFilters).toHaveBeenCalledTimes(1);
+ });
+
+ it('should reset the filtersChanged state to false', () => {
+ const container = shallow(
+
+ );
+ container.setState({
+ filtersChanged: true
+ });
+
+ container.instance().applyStagedFilters();
+
+ expect(container.state().filtersChanged).toBeFalsy();
+ });
+ });
+ describe('resetFilters', () => {
+ it('should reset all the staged filters to their initial states', () => {
+ const actions = Object.assign({}, mockActions, {
+ clearStagedFilters: jest.fn()
+ });
+
+ const container = shallow(
+
+ );
+
+ container.instance().resetFilters();
+ expect(actions.clearStagedFilters).toHaveBeenCalledTimes(1);
+ });
+ it('should reset all the applied filters to their initial states', () => {
+ const actions = Object.assign({}, mockActions, {
+ resetAppliedFilters: jest.fn()
+ });
+
+ const container = shallow(
+
+ );
+
+ container.instance().resetFilters();
+ expect(actions.resetAppliedFilters).toHaveBeenCalledTimes(1);
+ });
+ });
+});
\ No newline at end of file
diff --git a/tests/containers/search/mockRouter.js b/tests/containers/search/mockRouter.js
new file mode 100644
index 0000000000..8f68fa7e07
--- /dev/null
+++ b/tests/containers/search/mockRouter.js
@@ -0,0 +1,7 @@
+const Router = {
+ history: {
+ replace: jest.fn()
+ }
+};
+
+export default Router;
diff --git a/tests/containers/search/mockSearchHashes.js b/tests/containers/search/mockSearchHashes.js
index dc41555ab2..3936c273a8 100644
--- a/tests/containers/search/mockSearchHashes.js
+++ b/tests/containers/search/mockSearchHashes.js
@@ -1,4 +1,5 @@
import { initialState, filterStoreVersion } from 'redux/reducers/search/searchFiltersReducer';
+import { initialState as initialApplied } from 'redux/reducers/search/appliedFiltersReducer';
import * as FiscalYearHelper from 'helpers/fiscalYearHelper';
export const mockHash = {
@@ -37,11 +38,15 @@ export const mockFilters = {
export const mockRedux = {
filters: initialState,
+ appliedFilters: initialApplied,
params: {
hash: ''
}
};
export const mockActions = {
- populateAllSearchFilters: jest.fn()
+ populateAllSearchFilters: jest.fn(),
+ applyStagedFilters: jest.fn(),
+ setAppliedFilterEmptiness: jest.fn(),
+ setAppliedFilterCompletion: jest.fn()
};
diff --git a/tests/containers/search/mockSubmit.js b/tests/containers/search/mockSubmit.js
new file mode 100644
index 0000000000..631483ce4d
--- /dev/null
+++ b/tests/containers/search/mockSubmit.js
@@ -0,0 +1,15 @@
+import { initialState as initialApplied } from 'redux/reducers/search/appliedFiltersReducer'
+import { initialState as initialStaged } from 'redux/reducers/search/searchFiltersReducer'
+
+export const mockRedux = {
+ requestsComplete: true,
+ stagedFilters: initialStaged,
+ appliedFilters: initialApplied.filters
+};
+
+export const mockActions = {
+ applyStagedFilters: jest.fn(),
+ clearStagedFilters: jest.fn(),
+ setAppliedFilterCompletion: jest.fn(),
+ resetAppliedFilters: jest.fn()
+};
\ No newline at end of file
diff --git a/tests/containers/search/modals/fullDownload/mockFullDownload.js b/tests/containers/search/modals/fullDownload/mockFullDownload.js
index 98025d8de0..4c80f1b9e5 100644
--- a/tests/containers/search/modals/fullDownload/mockFullDownload.js
+++ b/tests/containers/search/modals/fullDownload/mockFullDownload.js
@@ -5,7 +5,8 @@ export const mockRedux = {
download: Object.assign({}, initialState, {
pendingDownload: false,
showCollapsedProgress: false,
- expectedFile: ''
+ expectedFile: '',
+ expectedUrl: ''
}),
filters: initialFilter
};
@@ -14,6 +15,7 @@ export const mockActions = {
setDownloadPending: jest.fn(),
setDownloadCollapsed: jest.fn(),
setDownloadExpectedFile: jest.fn(),
+ setDownloadExpectedUrl: jest.fn(),
resetDownload: jest.fn()
};
diff --git a/tests/containers/search/table/mockAwards.js b/tests/containers/search/table/mockAwards.js
index bf365d5fe7..d2bbb55cd6 100644
--- a/tests/containers/search/table/mockAwards.js
+++ b/tests/containers/search/table/mockAwards.js
@@ -6,11 +6,13 @@ import { initialState } from 'redux/reducers/search/searchFiltersReducer';
export const mockActions = {
toggleColumnVisibility: jest.fn(),
reorderColumns: jest.fn(),
- populateAvailableColumns: jest.fn()
+ populateAvailableColumns: jest.fn(),
+ setAppliedFilterCompletion: jest.fn()
};
export const mockRedux = {
filters: initialState,
+ noApplied: false,
columnVisibility: new VisibilityRecord()
};
diff --git a/tests/containers/search/topFilterBar/TopFilterBarContainer-test.jsx b/tests/containers/search/topFilterBar/TopFilterBarContainer-test.jsx
index 5da35df753..9c5cc3e0d1 100644
--- a/tests/containers/search/topFilterBar/TopFilterBarContainer-test.jsx
+++ b/tests/containers/search/topFilterBar/TopFilterBarContainer-test.jsx
@@ -5,7 +5,6 @@
import React from 'react';
import { mount } from 'enzyme';
-import sinon from 'sinon';
import { Set, OrderedMap } from 'immutable';
@@ -34,16 +33,14 @@ const defaultProps = {
const setup = (props) =>
mount();
-const prepareFiltersSpy = sinon.spy(TopFilterBarContainer.prototype, 'prepareFilters');
-
describe('TopFilterBarContainer', () => {
- it('should return a TopFilterBar child component with FY17 selected on load', () => {
+ it('should return a TopFilterBar child component with no filters selected by default', () => {
const topBarContainer = setup({
reduxFilters: initialState,
updateFilterCount: jest.fn()
});
- expect(topBarContainer.find(TopFilterBar)).toHaveLength(1);
+ expect(topBarContainer.find(TopFilterBar)).toHaveLength(0);
});
it('should return a TopFilterBar child component when there are active filters', () => {
@@ -85,12 +82,13 @@ describe('TopFilterBarContainer', () => {
// mount the container
const topBarContainer = setup(initialProps);
+ topBarContainer.instance().prepareFilters = jest.fn();
// change the props
topBarContainer.setProps(updatedProps);
// the prepareFilters function should have been called
- expect(prepareFiltersSpy.called).toBeTruthy();
+ expect(topBarContainer.instance().prepareFilters).toHaveBeenCalledTimes(1);
});
it('should update component state with Redux keyword filter when available', () => {
@@ -208,7 +206,7 @@ describe('TopFilterBarContainer', () => {
const filterItem = topBarContainer.state().filters[0];
const expectedFilterState = {
code: 'selectedLocations',
- name: 'Place of Performance Location',
+ name: 'Place of Performance',
scope: 'all',
values: [{
filter: {
diff --git a/tests/containers/search/visualizations/geo/GeoVisualizationSectionContainer-test.jsx b/tests/containers/search/visualizations/geo/GeoVisualizationSectionContainer-test.jsx
index 0c9d644a5d..6f492fd21e 100644
--- a/tests/containers/search/visualizations/geo/GeoVisualizationSectionContainer-test.jsx
+++ b/tests/containers/search/visualizations/geo/GeoVisualizationSectionContainer-test.jsx
@@ -4,7 +4,7 @@
*/
import React from 'react';
-import { mount } from 'enzyme';
+import { mount, shallow } from 'enzyme';
import sinon from 'sinon';
import { Set } from 'immutable';
@@ -14,6 +14,7 @@ import { GeoVisualizationSectionContainer } from
import MapBroadcaster from 'helpers/mapBroadcaster';
import { defaultFilters } from '../../../../testResources/defaultReduxFilters';
+import { geo as mockApi } from '../mockVisualizations';
jest.mock('helpers/searchHelper', () => require('./mocks/geoHelper'));
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
@@ -38,6 +39,7 @@ describe('GeoVisualizationSectionContainer', () => {
// mount the container
const container = mount();
expect(fetchDataSpy.callCount).toEqual(0);
@@ -64,6 +66,7 @@ describe('GeoVisualizationSectionContainer', () => {
// mount the container
const container =
mount();
@@ -86,6 +89,7 @@ describe('GeoVisualizationSectionContainer', () => {
it('should set the scope to place of performance when requested', () => {
// mount the container
const container = mount();
@@ -98,6 +102,7 @@ describe('GeoVisualizationSectionContainer', () => {
it('should set the scope to recipient when requested', () => {
// mount the container
const container = mount();
@@ -109,6 +114,7 @@ describe('GeoVisualizationSectionContainer', () => {
it('should request a map measurement operation if the scope has changed', () => {
const container = mount();
const mockPrepare = jest.fn();
@@ -123,6 +129,7 @@ describe('GeoVisualizationSectionContainer', () => {
it('should not request a map measurement operation if the scope has not changed', () => {
const container = mount();
const mockPrepare = jest.fn();
@@ -139,6 +146,7 @@ describe('GeoVisualizationSectionContainer', () => {
describe('mapLoaded', () => {
it('should set the loadingTiles state to false', () => {
const container = mount();
container.instance().mapLoaded();
@@ -148,6 +156,7 @@ describe('GeoVisualizationSectionContainer', () => {
it('should call the prepareFetch method', () => {
jest.useFakeTimers();
const container = mount();
const mockPrepare = jest.fn();
@@ -166,6 +175,7 @@ describe('GeoVisualizationSectionContainer', () => {
const attached = MapBroadcaster.on('measureMap', mockListener);
const container = mount();
container.setState({
@@ -186,6 +196,7 @@ describe('GeoVisualizationSectionContainer', () => {
const attached = MapBroadcaster.on('measureMap', mockListener);
const container = mount();
container.setState({
@@ -203,6 +214,7 @@ describe('GeoVisualizationSectionContainer', () => {
describe('receivedEntities', () => {
it('should set the state to the returned entities', () => {
const container = mount();
container.setState({
@@ -216,6 +228,7 @@ describe('GeoVisualizationSectionContainer', () => {
it('should make an API call using the returned entities', () => {
const container = mount();
const mockFetch = jest.fn();
@@ -232,14 +245,14 @@ describe('GeoVisualizationSectionContainer', () => {
});
describe('parseData', () => {
- it('should properly resture the API response for the map visualization', async () => {
+ it('should properly parse the API response for the map visualization', () => {
// mount the container
- const container = mount();
- container.instance().fetchData();
- await container.instance().apiRequest.promise;
+ container.instance().parseData(mockApi);
const expectedState = {
values: [123.12, 345.56],
@@ -265,6 +278,7 @@ describe('GeoVisualizationSectionContainer', () => {
describe('changeMapLayer', () => {
it('should update the mapLayer state when a new map tileset is requested', () => {
const container = mount();
expect(container.state().mapLayer).toEqual('state');
@@ -274,6 +288,7 @@ describe('GeoVisualizationSectionContainer', () => {
});
it('should make a new renderHash when a new map tileset is requested', () => {
const container = mount();
const originalHash = `${container.state().renderHash}`;
@@ -283,6 +298,7 @@ describe('GeoVisualizationSectionContainer', () => {
});
it('should update the mapLayer state when a new map tileset is requested', () => {
const container = mount();
container.setState({
@@ -296,6 +312,7 @@ describe('GeoVisualizationSectionContainer', () => {
});
it('should request a map measurement operation', () => {
const container = mount();
const mockPrepare = jest.fn();
diff --git a/tests/containers/search/visualizations/time/TimeVisualizationSectionContainer-test.jsx b/tests/containers/search/visualizations/time/TimeVisualizationSectionContainer-test.jsx
index f7bbca404f..a26c82da7b 100644
--- a/tests/containers/search/visualizations/time/TimeVisualizationSectionContainer-test.jsx
+++ b/tests/containers/search/visualizations/time/TimeVisualizationSectionContainer-test.jsx
@@ -5,7 +5,6 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
-import sinon from 'sinon';
import { Set } from 'immutable';
@@ -14,304 +13,127 @@ import { TimeVisualizationSectionContainer } from
import * as SearchHelper from 'helpers/searchHelper';
import { defaultFilters } from '../../../../testResources/defaultReduxFilters';
+import { mockActions, mockApi, mockQuarters, mockMonths } from './mockData';
-// force Jest to use native Node promises
-// see: https://facebook.github.io/jest/docs/troubleshooting.html#unresolved-promises
-global.Promise = require.requireActual('promise');
+jest.mock('helpers/searchHelper', () => require('./mockSearchHelper'));
-// spy on specific functions inside the component
-const fetchDataSpy = sinon.spy(TimeVisualizationSectionContainer.prototype, 'fetchData');
-
-// we don't want to actually hit the API because tests should be fully controlled, so we will mock
-// the SearchHelper functions
-const mockSearchHelper = (functionName, event, expectedResponse) => {
- jest.useFakeTimers();
- // override the specified function
- SearchHelper[functionName] = jest.fn(() => {
- // Axios normally returns a promise, replicate this, but return the expected result
- const networkCall = new Promise((resolve, reject) => {
- process.nextTick(() => {
- if (event === 'resolve') {
- resolve({
- data: expectedResponse
- });
- }
- else {
- reject({
- data: expectedResponse
- });
- }
- });
- });
-
- return {
- promise: networkCall,
- cancel: jest.fn()
- };
- });
-};
-
-const unmockSearchHelper = () => {
- jest.useRealTimers();
- jest.unmock('helpers/searchHelper');
-};
+jest.mock('components/search/visualizations/time/TimeVisualizationSection', () =>
+ jest.fn(() => null));
describe('TimeVisualizationSectionContainer', () => {
it('should make an API request on mount', () => {
- // create a mock API response
- const apiResponse = {
- page_metadata: {
- has_next_page: false,
- has_previous_page: false,
- next: null,
- page: 1,
- previous: null
- },
- results: [{
- item: '2013',
- aggregate: '1234'
- }],
- total_metadata: {
- count: 1
- }
- };
-
- // mock the search helper to resolve with the mocked response
- mockSearchHelper('performSpendingOverTimeSearch', 'resolve', apiResponse);
-
// mount the container
- mount();
+ const container = shallow();
+ container.instance().fetchAwards = jest.fn();
- // the mocked SearchHelper waits 1 tick to resolve the promise, so wait for the tick
- jest.runAllTicks();
+ container.instance().componentDidMount();
// everything should be updated now
- expect(fetchDataSpy.callCount).toEqual(1);
-
- // reset the mocks and spies
- unmockSearchHelper();
- fetchDataSpy.reset();
+ expect(container.instance().fetchAwards).toHaveBeenCalledTimes(1);
});
it('should make an API request when the Redux filters change', () => {
- // create a mock API response
- const apiResponse = {
- page_metadata: {
- has_next_page: false,
- has_previous_page: false,
- next: null,
- page: 1,
- previous: null
- },
- results: [{
- item: '2013',
- aggregate: '1234'
- }],
- total_metadata: {
- count: 1
- }
- };
-
- // mock the search helper to resolve with the mocked response
- mockSearchHelper('performSpendingOverTimeSearch', 'resolve', apiResponse);
-
- const initialFilters = Object.assign({}, defaultFilters);
- const secondFilters = Object.assign({}, defaultFilters, {
+ const mockFilters = Object.assign({}, defaultFilters, {
timePeriodType: 'fy',
timePeriodFY: new Set(['2014', '2015'])
});
// mount the container
- const timeVisualizationContainer =
- mount();
+ const container =
+ mount();
+ container.instance().fetchAwards = jest.fn();
- // wait for the first SearchHelper call to finish
- jest.runAllTicks();
+ expect(container.instance().fetchAwards).toHaveBeenCalledTimes(0);
- // the first API call should have been called
- expect(fetchDataSpy.callCount).toEqual(1);
-
- // now update the props
- timeVisualizationContainer.setProps({
- reduxFilters: secondFilters
+ container.setProps({
+ reduxFilters: mockFilters
});
- // wait for the second SearchHelper call to finish
- jest.runAllTicks();
- // the first API call should have been called
- expect(fetchDataSpy.callCount).toEqual(2);
-
- // reset the mocks and spies
- unmockSearchHelper();
- fetchDataSpy.reset();
+ expect(container.instance().fetchAwards).toHaveBeenCalledTimes(1);
});
describe('parseData', () => {
it('should properly restructure the API data for the spending over time chart for fiscal year series', () => {
- // create a mock API response
- const apiResponse = {
- page_metadata: {
- has_next_page: false,
- has_previous_page: false,
- next: null,
- page: 1,
- previous: null
- },
- results: [{
- time_period: {
- fiscal_year: "2016"
- },
- aggregated_amount: "1234"
- },
- {
- time_period: {
- fiscal_year: "2017"
- },
- aggregated_amount: "5555"
- }]
- };
-
- const mockReduxActions = {
- setVizTxnSum: jest.fn()
- };
-
- // mock the search helper to resolve with the mocked response
- mockSearchHelper('performSpendingOverTimeSearch', 'resolve', apiResponse);
// mount the container
- const timeVisualizationContainer =
+ const container =
shallow();
- timeVisualizationContainer.instance().parseData(apiResponse, 'fiscal_year');
+ container.instance().parseData(mockApi, 'fiscal_year');
- // wait for the SearchHelper promises to resolve
- jest.runAllTicks();
// validate the state contains the correctly parsed values
const expectedState = {
loading: false,
+ error: false,
visualizationPeriod: "fiscal_year",
- groups: ['2016', '2017'],
- xSeries: [['2016'], ['2017']],
- ySeries: [[1234], [5555]]
+ groups: ['1979', '1980'],
+ xSeries: [['1979'], ['1980']],
+ ySeries: [[123], [234]],
+ rawLabels:[{period: null, year:'1979'},{period: null, year:'1980'}]
};
- expect(timeVisualizationContainer.state()).toEqual(expectedState);
+ expect(container.state()).toEqual(expectedState);
});
-
it('should properly restructure the API data for the spending over time chart for quarterly series', () => {
- // create a mock API response
- const apiResponse = {
- page_metadata: {
- has_next_page: false,
- has_previous_page: false,
- next: null,
- page: 1,
- previous: null
- },
- results: [{
- time_period: {
- fiscal_year: "2017",
- quarter: "1"
- },
- aggregated_amount: "1234"
- },
- {
- time_period: {
- fiscal_year: "2017",
- quarter: "2"
- },
- aggregated_amount: "5555"
- }]
- };
-
- const mockReduxActions = {
- setVizTxnSum: jest.fn()
- };
-
- // mock the search helper to resolve with the mocked response
- mockSearchHelper('performSpendingOverTimeSearch', 'resolve', apiResponse);
-
// mount the container
- const timeVisualizationContainer =
+ const container =
shallow();
- timeVisualizationContainer.instance().updateVisualizationPeriod('quarter');
+ container.setState({
+ visualizationPeriod: 'quarter'
+ });
- timeVisualizationContainer.instance().parseData(apiResponse, 'quarter');
+ container.instance().parseData(mockQuarters, 'quarter');
- // wait for the SearchHelper promises to resolve
- jest.runAllTicks();
// validate the state contains the correctly parsed values
- const expectedState = {
+ const expectedState = {
loading: false,
+ error: false,
visualizationPeriod: "quarter",
- groups: ['Q1 2017', 'Q2 2017'],
- xSeries: [['Q1 2017'], ['Q2 2017']],
- ySeries: [[1234], [5555]]
+ groups: ['Q1 1979', 'Q2 1979'],
+ xSeries: [['Q1 1979'], ['Q2 1979']],
+ ySeries: [[1234], [5555]],
+ rawLabels:[{period: 'Q1', year:'1979'},{period: 'Q2', year:'1979'}]
};
- expect(timeVisualizationContainer.state()).toEqual(expectedState);
+ expect(container.state()).toEqual(expectedState);
});
it('should properly restructure the API data for the spending over time chart for monthly series', () => {
- // create a mock API response
- const apiResponse = {
- page_metadata: {
- has_next_page: false,
- has_previous_page: false,
- next: null,
- page: 1,
- previous: null
- },
- results: [{
- time_period: {
- fiscal_year: "2017",
- month: "1"
- },
- aggregated_amount: "1234"
- },
- {
- time_period: {
- fiscal_year: "2017",
- month: "2"
- },
- aggregated_amount: "5555"
- }]
- };
-
- const mockReduxActions = {
- setVizTxnSum: jest.fn()
- };
-
- // mock the search helper to resolve with the mocked response
- mockSearchHelper('performSpendingOverTimeSearch', 'resolve', apiResponse);
-
// mount the container
- const timeVisualizationContainer =
+ const container =
shallow();
- timeVisualizationContainer.instance().updateVisualizationPeriod('month');
+ container.setState({
+ visualizationPeriod: 'month'
+ });
+
+ container.instance().parseData(mockMonths, 'month');
+
- timeVisualizationContainer.instance().parseData(apiResponse, 'month');
- // wait for the SearchHelper promises to resolve
- jest.runAllTicks();
// validate the state contains the correctly parsed values
- const expectedState = {
+ const expectedState = {
loading: false,
+ error: false,
visualizationPeriod: "month",
- groups: ['Oct 2016', 'Nov 2016'],
- xSeries: [['Oct 2016'], ['Nov 2016']],
- ySeries: [[1234], [5555]]
+ groups: ['Oct 1978', 'Nov 1978'],
+ xSeries: [['Oct 1978'], ['Nov 1978']],
+ ySeries: [[1234], [5555]],
+ rawLabels:[{period: 'Oct', year:'1978'},{period: 'Nov', year:'1978'}]
};
- expect(timeVisualizationContainer.state()).toEqual(expectedState);
+ expect(container.state()).toEqual(expectedState);
});
});
});
diff --git a/tests/containers/search/visualizations/time/mockData.js b/tests/containers/search/visualizations/time/mockData.js
new file mode 100644
index 0000000000..87499a136f
--- /dev/null
+++ b/tests/containers/search/visualizations/time/mockData.js
@@ -0,0 +1,56 @@
+export const mockApi = {
+ results: [
+ {
+ time_period: {
+ fiscal_year: '1979'
+ },
+ aggregated_amount: 123,
+ group: 'fiscal_Year'
+ },
+ {
+ time_period: {
+ fiscal_year: '1980'
+ },
+ aggregated_amount: 234,
+ group: 'fiscal_Year'
+ }
+ ]
+};
+
+export const mockQuarters = {
+ results: [{
+ time_period: {
+ fiscal_year: "1979",
+ quarter: "1"
+ },
+ aggregated_amount: "1234"
+ },
+ {
+ time_period: {
+ fiscal_year: "1979",
+ quarter: "2"
+ },
+ aggregated_amount: "5555"
+ }]
+};
+
+export const mockMonths = {
+ results: [{
+ time_period: {
+ fiscal_year: "1979",
+ month: "1"
+ },
+ aggregated_amount: "1234"
+ },
+ {
+ time_period: {
+ fiscal_year: "1979",
+ month: "2"
+ },
+ aggregated_amount: "5555"
+ }]
+};
+
+export const mockActions = {
+ setAppliedFilterCompletion: jest.fn()
+};
\ No newline at end of file
diff --git a/tests/containers/search/visualizations/time/mockSearchHelper.js b/tests/containers/search/visualizations/time/mockSearchHelper.js
new file mode 100644
index 0000000000..42cd44edfe
--- /dev/null
+++ b/tests/containers/search/visualizations/time/mockSearchHelper.js
@@ -0,0 +1,14 @@
+import { mockApi } from './mockData';
+
+export const performSpendingOverTimeSearch = () => (
+ {
+ promise: new Promise((resolve) => {
+ process.nextTick(() => {
+ resolve({
+ data: mockApi
+ });
+ })
+ }),
+ cancel: jest.fn()
+ }
+);
diff --git a/tests/redux/reducers/search/appliedFiltersReducer-test.js b/tests/redux/reducers/search/appliedFiltersReducer-test.js
new file mode 100644
index 0000000000..1e333404e4
--- /dev/null
+++ b/tests/redux/reducers/search/appliedFiltersReducer-test.js
@@ -0,0 +1,79 @@
+
+import { Set } from 'immutable';
+
+import appliedFiltersReducer, { initialState } from 'redux/reducers/search/appliedFiltersReducer';
+
+describe('appliedFiltersReducer', () => {
+ it('should return the initial state by default', () => {
+ expect(
+ appliedFiltersReducer(undefined, {})
+ ).toEqual(initialState);
+ });
+
+ describe('APPLY_STAGED_FILTERS', () => {
+ it('should set the filter object to the provided object', () => {
+ const newFilters = Object.assign({}, initialState.filters, {
+ timePeriodFY: new Set(['1990'])
+ });
+
+ const action = {
+ type: 'APPLY_STAGED_FILTERS',
+ filters: newFilters
+ };
+
+ const newState = appliedFiltersReducer(undefined, action);
+ expect(newState.filters.timePeriodFY).toEqual(new Set(['1990']));
+ });
+ });
+
+ describe('CLEAR_APPLIED_FILTERS', () => {
+ it('should should return the initial state', () => {
+ const newFilters = Object.assign({}, initialState.filters, {
+ timePeriodFY: new Set(['1990'])
+ });
+
+ const modifiedState = {
+ filters: newFilters,
+ _empty: false,
+ _complete: false
+ };
+
+ expect(modifiedState).not.toEqual(initialState);
+
+ const action = {
+ type: 'CLEAR_APPLIED_FILTERS'
+ };
+
+ const restoredState = appliedFiltersReducer(modifiedState, action);
+ expect(restoredState).toEqual(initialState);
+ });
+ });
+
+ describe('SET_APPLIED_FILTER_EMPTINESS', () => {
+ it('should set the _empty value', () => {
+ let state = appliedFiltersReducer(undefined, {});
+ expect(state._empty).toBeTruthy();
+
+ const action = {
+ type: 'SET_APPLIED_FILTER_EMPTINESS',
+ empty: false
+ };
+ state = appliedFiltersReducer(state, action);
+ expect(state._empty).toBeFalsy();
+ });
+ });
+
+ describe('SET_APPLIED_FILTER_COMPLETION', () => {
+ it('should set the _complete value', () => {
+ let state = appliedFiltersReducer(undefined, {});
+ expect(state._complete).toBeTruthy();
+
+ const action = {
+ type: 'SET_APPLIED_FILTER_COMPLETION',
+ complete: false
+ };
+ state = appliedFiltersReducer(state, action);
+ expect(state._complete).toBeFalsy();
+ });
+ });
+});
\ No newline at end of file
diff --git a/tests/redux/reducers/search/searchFiltersReducer-test.js b/tests/redux/reducers/search/searchFiltersReducer-test.js
index 4a65d6bd35..f7c02a2bee 100644
--- a/tests/redux/reducers/search/searchFiltersReducer-test.js
+++ b/tests/redux/reducers/search/searchFiltersReducer-test.js
@@ -773,7 +773,7 @@ describe('searchFiltersReducer', () => {
const expectedSecond = {
timePeriodType: 'fy',
- timePeriodFY: new Set(['1991']),
+ timePeriodFY: new Set(),
timePeriodStart: null,
timePeriodEnd: null
};
@@ -817,7 +817,7 @@ describe('searchFiltersReducer', () => {
const expectedSecond = {
timePeriodType: 'fy',
- timePeriodFY: new Set(['1991']),
+ timePeriodFY: new Set(),
timePeriodStart: null,
timePeriodEnd: null
};