Commit ee28174e authored by Ezekiel Kigbo's avatar Ezekiel Kigbo Committed by Mike Greiling

Move vuex content to duration_chart module

Creates a new modules directory to
separate the cycle_analytics vuex store

Creates separate vuex files for the
duration chart component

Move vuex boilerplate into duration chart component
parent 0a0077c3
......@@ -53,7 +53,6 @@ export default {
'isLoading',
'isLoadingStage',
'isLoadingTasksByTypeChart',
'isLoadingDurationChart',
'isEmptyStage',
'isSavingCustomStage',
'isCreatingCustomStage',
......@@ -76,9 +75,7 @@ export default {
...mapGetters([
'hasNoAccessError',
'currentGroupPath',
'durationChartPlottableData',
'tasksByTypeChartData',
'durationChartMedianData',
'activeStages',
'selectedProjectIds',
'enableCustomOrdering',
......@@ -94,14 +91,11 @@ export default {
return this.selectedGroup && !this.errorCode;
},
shouldDisplayDurationChart() {
return this.featureFlags.hasDurationChart && !this.hasNoAccessError;
return this.featureFlags.hasDurationChart && !this.hasNoAccessError && !this.isLoading;
},
shouldDisplayTasksByTypeChart() {
return this.featureFlags.hasTasksByTypeChart && !this.hasNoAccessError;
},
isDurationChartLoaded() {
return !this.isLoadingDurationChart && !this.isLoading;
},
isTasksByTypeChartLoaded() {
return !this.isLoading && !this.isLoadingTasksByTypeChart;
},
......@@ -156,7 +150,6 @@ export default {
'showEditCustomStageForm',
'setDateRange',
'fetchTasksByTypeData',
'updateSelectedDurationChartStages',
'createCustomStage',
'updateStage',
'removeStage',
......@@ -194,9 +187,6 @@ export default {
onRemoveStage(id) {
this.removeStage(id);
},
onDurationStageSelect(stages) {
this.updateSelectedDurationChartStages(stages);
},
onStageReorder(data) {
this.reorderStage(data);
},
......@@ -321,13 +311,7 @@ export default {
</div>
</div>
<div v-if="shouldDisplayDurationChart" class="mt-3">
<duration-chart
:is-loading="isLoading"
:stages="activeStages"
:scatter-data="durationChartPlottableData"
:median-line-data="durationChartMedianData"
@stageSelected="onDurationStageSelect"
/>
<duration-chart :stages="activeStages" />
</div>
<template v-if="shouldDisplayTasksByTypeChart">
<div class="js-tasks-by-type-chart">
......
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import { dateFormats } from '../../shared/constants';
import Scatterplot from '../../shared/components/scatterplot.vue';
......@@ -12,32 +13,25 @@ export default {
StageDropdownFilter,
},
props: {
isLoading: {
type: Boolean,
required: false,
default: false,
},
stages: {
type: Array,
required: true,
},
scatterData: {
type: Array,
required: true,
},
medianLineData: {
type: Array,
required: true,
},
},
computed: {
...mapState('durationChart', ['isLoading']),
...mapGetters('durationChart', ['durationChartPlottableData', 'durationChartMedianData']),
hasData() {
return Boolean(this.scatterData.length);
return Boolean(this.durationChartPlottableData.length);
},
},
mounted() {
this.fetchDurationData();
},
methods: {
onSelectStage(selectedStages) {
this.$emit('stageSelected', selectedStages);
...mapActions('durationChart', ['fetchDurationData', 'updateSelectedDurationChartStages']),
onDurationStageSelect(stages) {
this.updateSelectedDurationChartStages(stages);
},
},
durationChartTooltipDateFormat: dateFormats.defaultDate,
......@@ -53,7 +47,7 @@ export default {
v-if="stages.length"
class="ml-auto"
:stages="stages"
@selected="onSelectStage"
@selected="onDurationStageSelect"
/>
</div>
<scatterplot
......@@ -61,8 +55,8 @@ export default {
:x-axis-title="s__('CycleAnalytics|Date')"
:y-axis-title="s__('CycleAnalytics|Total days to completion')"
:tooltip-date-format="$options.durationChartTooltipDateFormat"
:scatter-data="scatterData"
:median-line-data="medianLineData"
:scatter-data="durationChartPlottableData"
:median-line-data="durationChartMedianData"
/>
<div v-else ref="duration-chart-no-data" class="bs-callout bs-callout-info">
{{ __('There is no data available. Please change your selection.') }}
......
import dateFormat from 'dateformat';
import Api from 'ee/api';
import { getDayDifference, getDateInPast } from '~/lib/utils/datetime_utility';
import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
import httpStatus from '~/lib/utils/http_status';
import * as types from './mutation_types';
import { dateFormats } from '../../shared/constants';
import { removeFlash } from '../utils';
const handleErrorOrRethrow = ({ action, error }) => {
......@@ -111,9 +108,8 @@ export const requestCycleAnalyticsData = ({ commit }) => commit(types.REQUEST_CY
export const receiveCycleAnalyticsDataSuccess = ({ state, commit, dispatch }) => {
commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS);
const { featureFlags: { hasDurationChart = false, hasTasksByTypeChart = false } = {} } = state;
const { featureFlags: { hasTasksByTypeChart = false } = {} } = state;
const promises = [];
if (hasDurationChart) promises.push('fetchDurationData');
if (hasTasksByTypeChart) promises.push('fetchTopRankedGroupLabels');
return Promise.all(promises.map(func => dispatch(func)));
};
......@@ -405,125 +401,6 @@ export const removeStage = ({ dispatch, state }, stageId) => {
.catch(error => dispatch('receiveRemoveStageError', error));
};
export const requestDurationData = ({ commit }) => commit(types.REQUEST_DURATION_DATA);
export const receiveDurationDataSuccess = ({ commit, state, dispatch }, data) => {
commit(types.RECEIVE_DURATION_DATA_SUCCESS, data);
const { featureFlags: { hasDurationChartMedian = false } = {} } = state;
if (hasDurationChartMedian) dispatch('fetchDurationMedianData');
};
export const receiveDurationDataError = ({ commit }) => {
commit(types.RECEIVE_DURATION_DATA_ERROR);
createFlash(__('There was an error while fetching value stream analytics duration data.'));
};
export const fetchDurationData = ({ state, dispatch, getters }) => {
dispatch('requestDurationData');
const {
stages,
selectedGroup: { fullPath },
} = state;
const {
cycleAnalyticsRequestParams: { created_after, created_before, project_ids },
} = getters;
return Promise.all(
stages.map(stage => {
const { slug } = stage;
return Api.cycleAnalyticsDurationChart(fullPath, slug, {
created_after,
created_before,
project_ids,
}).then(({ data }) => ({
slug,
selected: true,
data,
}));
}),
)
.then(data => {
dispatch('receiveDurationDataSuccess', data);
})
.catch(() => dispatch('receiveDurationDataError'));
};
export const requestDurationMedianData = ({ commit }) => commit(types.REQUEST_DURATION_MEDIAN_DATA);
export const receiveDurationMedianDataSuccess = ({ commit }, data) =>
commit(types.RECEIVE_DURATION_MEDIAN_DATA_SUCCESS, data);
export const receiveDurationMedianDataError = ({ commit }) => {
commit(types.RECEIVE_DURATION_MEDIAN_DATA_ERROR);
createFlash(__('There was an error while fetching value stream analytics duration median data.'));
};
export const fetchDurationMedianData = ({ state, dispatch, getters }) => {
dispatch('requestDurationMedianData');
const {
stages,
selectedGroup: { fullPath },
startDate,
endDate,
} = state;
const {
cycleAnalyticsRequestParams: { project_ids },
} = getters;
const offsetValue = getDayDifference(new Date(startDate), new Date(endDate));
const offsetCreatedAfter = getDateInPast(new Date(startDate), offsetValue);
const offsetCreatedBefore = getDateInPast(new Date(endDate), offsetValue);
return Promise.all(
stages.map(stage => {
const { slug } = stage;
return Api.cycleAnalyticsDurationChart(fullPath, slug, {
created_after: dateFormat(offsetCreatedAfter, dateFormats.isoDate),
created_before: dateFormat(offsetCreatedBefore, dateFormats.isoDate),
project_ids,
}).then(({ data }) => ({
slug,
selected: true,
data,
}));
}),
)
.then(data => {
dispatch('receiveDurationMedianDataSuccess', data);
})
.catch(() => dispatch('receiveDurationMedianDataError'));
};
export const updateSelectedDurationChartStages = ({ state, commit }, stages) => {
const setSelectedPropertyOnStages = data =>
data.map(stage => {
const selected = stages.reduce((result, object) => {
if (object.slug === stage.slug) return true;
return result;
}, false);
return {
...stage,
selected,
};
});
const { durationData, durationMedianData } = state;
const updatedDurationStageData = setSelectedPropertyOnStages(durationData);
const updatedDurationStageMedianData = setSelectedPropertyOnStages(durationMedianData);
commit(types.UPDATE_SELECTED_DURATION_CHART_STAGES, {
updatedDurationStageData,
updatedDurationStageMedianData,
});
};
export const setTasksByTypeFilters = ({ dispatch, commit }, data) => {
commit(types.SET_TASKS_BY_TYPE_FILTERS, data);
dispatch('fetchTasksByTypeData');
......
......@@ -2,7 +2,7 @@ import dateFormat from 'dateformat';
import { isNumber } from 'lodash';
import httpStatus from '~/lib/utils/http_status';
import { dateFormats } from '../../shared/constants';
import { getDurationChartData, getDurationChartMedianData, getTasksByTypeData } from '../utils';
import { getTasksByTypeData } from '../utils';
export const hasNoAccessError = state => state.errorCode === httpStatus.FORBIDDEN;
......@@ -18,26 +18,6 @@ export const cycleAnalyticsRequestParams = ({ startDate = null, endDate = null }
created_before: endDate ? dateFormat(endDate, dateFormats.isoDate) : null,
});
export const durationChartPlottableData = state => {
const { durationData, startDate, endDate } = state;
const selectedStagesDurationData = durationData.filter(stage => stage.selected);
const plottableData = getDurationChartData(selectedStagesDurationData, startDate, endDate);
return plottableData.length ? plottableData : [];
};
export const durationChartMedianData = state => {
const { durationMedianData, startDate, endDate } = state;
const selectedStagesDurationMedianData = durationMedianData.filter(stage => stage.selected);
const plottableData = getDurationChartMedianData(
selectedStagesDurationMedianData,
startDate,
endDate,
);
return plottableData.length ? plottableData : [];
};
export const tasksByTypeChartData = ({ tasksByType, startDate, endDate }) => {
if (tasksByType && tasksByType.data.length) {
return getTasksByTypeData({
......
......@@ -4,6 +4,7 @@ import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
import durationChart from './modules/duration_chart/index';
Vue.use(Vuex);
......@@ -13,4 +14,5 @@ export default () =>
getters,
mutations,
state,
modules: { durationChart },
});
import dateFormat from 'dateformat';
import Api from 'ee/api';
import { getDayDifference, getDateInPast } from '~/lib/utils/datetime_utility';
import createFlash from '~/flash';
import { __ } from '~/locale';
import * as types from './mutation_types';
import { dateFormats } from '../../../../shared/constants';
export const requestDurationData = ({ commit }) => commit(types.REQUEST_DURATION_DATA);
export const receiveDurationDataSuccess = ({ commit, rootState, dispatch }, data) => {
commit(types.RECEIVE_DURATION_DATA_SUCCESS, data);
const { featureFlags: { hasDurationChartMedian = false } = {} } = rootState;
if (hasDurationChartMedian) dispatch('fetchDurationMedianData');
};
export const receiveDurationDataError = ({ commit }) => {
commit(types.RECEIVE_DURATION_DATA_ERROR);
createFlash(__('There was an error while fetching value stream analytics duration data.'));
};
export const fetchDurationData = ({ dispatch, rootGetters, rootState }) => {
dispatch('requestDurationData');
const {
stages,
selectedGroup: { fullPath },
} = rootState;
const {
cycleAnalyticsRequestParams: { created_after, created_before, project_ids },
} = rootGetters;
return Promise.all(
stages.map(stage => {
const { slug } = stage;
return Api.cycleAnalyticsDurationChart(fullPath, slug, {
created_after,
created_before,
project_ids,
}).then(({ data }) => ({
slug,
selected: true,
data,
}));
}),
)
.then(data => dispatch('receiveDurationDataSuccess', data))
.catch(() => dispatch('receiveDurationDataError'));
};
export const receiveDurationMedianDataSuccess = ({ commit }, data) =>
commit(types.RECEIVE_DURATION_MEDIAN_DATA_SUCCESS, data);
export const receiveDurationMedianDataError = ({ commit }) => {
commit(types.RECEIVE_DURATION_MEDIAN_DATA_ERROR);
createFlash(__('There was an error while fetching value stream analytics duration median data.'));
};
export const fetchDurationMedianData = ({ dispatch, rootState, rootGetters }) => {
const {
stages,
selectedGroup: { fullPath },
startDate,
endDate,
} = rootState;
const {
cycleAnalyticsRequestParams: { project_ids },
} = rootGetters;
const offsetValue = getDayDifference(new Date(startDate), new Date(endDate));
const offsetCreatedAfter = getDateInPast(new Date(startDate), offsetValue);
const offsetCreatedBefore = getDateInPast(new Date(endDate), offsetValue);
return Promise.all(
stages.map(stage => {
const { slug } = stage;
return Api.cycleAnalyticsDurationChart(fullPath, slug, {
created_after: dateFormat(offsetCreatedAfter, dateFormats.isoDate),
created_before: dateFormat(offsetCreatedBefore, dateFormats.isoDate),
project_ids,
}).then(({ data }) => ({
slug,
selected: true,
data,
}));
}),
)
.then(data => dispatch('receiveDurationMedianDataSuccess', data))
.catch(() => dispatch('receiveDurationMedianDataError'));
};
export const updateSelectedDurationChartStages = ({ state, commit }, stages) => {
const setSelectedPropertyOnStages = data =>
data.map(stage => {
const selected = stages.reduce((result, object) => {
if (object.slug === stage.slug) return true;
return result;
}, false);
return {
...stage,
selected,
};
});
const { durationData, durationMedianData } = state;
const updatedDurationStageData = setSelectedPropertyOnStages(durationData);
const updatedDurationStageMedianData = setSelectedPropertyOnStages(durationMedianData);
commit(types.UPDATE_SELECTED_DURATION_CHART_STAGES, {
updatedDurationStageData,
updatedDurationStageMedianData,
});
};
import { getDurationChartData, getDurationChartMedianData } from '../../../utils';
export const durationChartPlottableData = (state, _, rootState) => {
const { startDate, endDate } = rootState;
const { durationData } = state;
const selectedStagesDurationData = durationData.filter(stage => stage.selected);
const plottableData = getDurationChartData(selectedStagesDurationData, startDate, endDate);
return plottableData.length ? plottableData : [];
};
export const durationChartMedianData = (state, _, rootState) => {
const { startDate, endDate } = rootState;
const { durationMedianData } = state;
const selectedStagesDurationMedianData = durationMedianData.filter(stage => stage.selected);
const plottableData = getDurationChartMedianData(
selectedStagesDurationMedianData,
startDate,
endDate,
);
return plottableData.length ? plottableData : [];
};
import state from './state';
import mutations from './mutations';
import * as getters from './getters';
import * as actions from './actions';
export default {
namespaced: true,
state,
mutations,
getters,
actions,
};
export const UPDATE_SELECTED_DURATION_CHART_STAGES = 'UPDATE_SELECTED_DURATION_CHART_STAGES';
export const REQUEST_DURATION_DATA = 'REQUEST_DURATION_DATA';
export const RECEIVE_DURATION_DATA_SUCCESS = 'RECEIVE_DURATION_DATA_SUCCESS';
export const RECEIVE_DURATION_DATA_ERROR = 'RECEIVE_DURATION_DATA_ERROR';
export const RECEIVE_DURATION_MEDIAN_DATA_SUCCESS = 'RECEIVE_DURATION_MEDIAN_DATA_SUCCESS';
export const RECEIVE_DURATION_MEDIAN_DATA_ERROR = 'RECEIVE_DURATION_MEDIAN_DATA_ERROR';
import * as types from './mutation_types';
export default {
[types.UPDATE_SELECTED_DURATION_CHART_STAGES](
state,
{ updatedDurationStageData, updatedDurationStageMedianData },
) {
state.durationData = updatedDurationStageData;
state.durationMedianData = updatedDurationStageMedianData;
},
[types.REQUEST_DURATION_DATA](state) {
state.isLoading = true;
},
[types.RECEIVE_DURATION_DATA_SUCCESS](state, data) {
state.durationData = data;
state.isLoading = false;
},
[types.RECEIVE_DURATION_DATA_ERROR](state) {
state.durationData = [];
state.isLoading = false;
},
[types.RECEIVE_DURATION_MEDIAN_DATA_SUCCESS](state, data) {
state.durationMedianData = data;
},
[types.RECEIVE_DURATION_MEDIAN_DATA_ERROR](state) {
state.durationMedianData = [];
},
};
export default () => ({
isLoading: false,
durationData: [],
durationMedianData: [],
});
......@@ -5,8 +5,6 @@ export const SET_SELECTED_PROJECTS = 'SET_SELECTED_PROJECTS';
export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE';
export const SET_DATE_RANGE = 'SET_DATE_RANGE';
export const UPDATE_SELECTED_DURATION_CHART_STAGES = 'UPDATE_SELECTED_DURATION_CHART_STAGES';
export const REQUEST_CYCLE_ANALYTICS_DATA = 'REQUEST_CYCLE_ANALYTICS_DATA';
export const RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS = 'RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS';
export const RECEIVE_CYCLE_ANALYTICS_DATA_ERROR = 'RECEIVE_CYCLE_ANALYTICS_DATA_ERROR';
......@@ -50,14 +48,6 @@ export const RECEIVE_UPDATE_STAGE_ERROR = 'RECEIVE_UPDATE_STAGE_ERROR';
export const REQUEST_REMOVE_STAGE = 'REQUEST_REMOVE_STAGE';
export const RECEIVE_REMOVE_STAGE_RESPONSE = 'RECEIVE_REMOVE_STAGE_RESPONSE';
export const REQUEST_DURATION_DATA = 'REQUEST_DURATION_DATA';
export const RECEIVE_DURATION_DATA_SUCCESS = 'RECEIVE_DURATION_DATA_SUCCESS';
export const RECEIVE_DURATION_DATA_ERROR = 'RECEIVE_DURATION_DATA_ERROR';
export const REQUEST_DURATION_MEDIAN_DATA = 'REQUEST_DURATION_MEDIAN_DATA';
export const RECEIVE_DURATION_MEDIAN_DATA_SUCCESS = 'RECEIVE_DURATION_MEDIAN_DATA_SUCCESS';
export const RECEIVE_DURATION_MEDIAN_DATA_ERROR = 'RECEIVE_DURATION_MEDIAN_DATA_ERROR';
export const SET_TASKS_BY_TYPE_FILTERS = 'SET_TASKS_BY_TYPE_FILTERS';
export const INITIALIZE_CYCLE_ANALYTICS = 'INITIALIZE_CYCLE_ANALYTICS';
......
......@@ -21,13 +21,6 @@ export default {
state.startDate = startDate;
state.endDate = endDate;
},
[types.UPDATE_SELECTED_DURATION_CHART_STAGES](
state,
{ updatedDurationStageData, updatedDurationStageMedianData },
) {
state.durationData = updatedDurationStageData;
state.durationMedianData = updatedDurationStageMedianData;
},
[types.REQUEST_CYCLE_ANALYTICS_DATA](state) {
state.isLoading = true;
state.isCreatingCustomStage = false;
......@@ -181,28 +174,6 @@ export default {
[types.RECEIVE_REMOVE_STAGE_RESPONSE](state) {
state.isLoading = false;
},
[types.REQUEST_DURATION_DATA](state) {
state.isLoadingDurationChart = true;
},
[types.RECEIVE_DURATION_DATA_SUCCESS](state, data) {
state.durationData = data;
state.isLoadingDurationChart = false;
},
[types.RECEIVE_DURATION_DATA_ERROR](state) {
state.durationData = [];
state.isLoadingDurationChart = false;
},
[types.REQUEST_DURATION_MEDIAN_DATA](state) {
state.isLoadingDurationChartMedianData = true;
},
[types.RECEIVE_DURATION_MEDIAN_DATA_SUCCESS](state, data) {
state.durationMedianData = data;
state.isLoadingDurationChartMedianData = false;
},
[types.RECEIVE_DURATION_MEDIAN_DATA_ERROR](state) {
state.durationMedianData = [];
state.isLoadingDurationChartMedianData = false;
},
[types.SET_TASKS_BY_TYPE_FILTERS](state, { filter, value }) {
const {
tasksByType: { selectedLabelIds, ...tasksByTypeRest },
......
......@@ -9,8 +9,6 @@ export default () => ({
isLoading: false,
isLoadingStage: false,
isLoadingTasksByTypeChart: false,
isLoadingDurationChart: false,
isLoadingDurationChartMedianData: false,
isEmptyStage: false,
errorCode: null,
......@@ -41,7 +39,4 @@ export default () => ({
selectedLabelIds: [],
data: [],
},
durationData: [],
durationMedianData: [],
});
......@@ -176,7 +176,7 @@ describe('Cycle Analytics component', () => {
displaysDateRangePicker(false);
});
it('does not display the summary table', () => {
it('does not display the recent activity table', () => {
displaysRecentActivityCard(false);
});
......@@ -233,7 +233,7 @@ describe('Cycle Analytics component', () => {
displaysDateRangePicker(true);
});
it('displays the summary table', () => {
it('displays the recent activity table', () => {
displaysRecentActivityCard(true);
});
......@@ -259,10 +259,6 @@ describe('Cycle Analytics component', () => {
startDate: mockData.startDate,
endDate: mockData.endDate,
});
wrapper.vm.$store.dispatch(
'receiveDurationDataSuccess',
mockData.transformedDurationData,
);
});
it('displays the duration chart', () => {
......@@ -277,7 +273,6 @@ describe('Cycle Analytics component', () => {
opts: {
stubs: {
'stage-event-list': true,
'summary-table': true,
'add-stage-button': true,
'stage-table-header': true,
},
......@@ -335,7 +330,7 @@ describe('Cycle Analytics component', () => {
displaysDateRangePicker(false);
});
it('does not display the summary table', () => {
it('does not display the recent activity table', () => {
displaysRecentActivityCard(false);
});
......@@ -424,7 +419,6 @@ describe('Cycle Analytics component', () => {
overrides = {},
mockFetchStageData = true,
mockFetchStageMedian = true,
mockFetchDurationData = true,
mockFetchTasksByTypeData = true,
mockFetchTopRankedGroupLabels = true,
}) => {
......@@ -450,12 +444,6 @@ describe('Cycle Analytics component', () => {
.reply(defaultStatus, { ...mockData.tasksByTypeData });
}
if (mockFetchDurationData) {
mock
.onGet(mockData.endpoints.durationData)
.reply(defaultStatus, [...mockData.rawDurationData]);
}
if (mockFetchStageMedian) {
mock.onGet(mockData.endpoints.stageMedian).reply(defaultStatus, { value: null });
}
......@@ -536,22 +524,6 @@ describe('Cycle Analytics component', () => {
);
});
it('will display an error if the fetchDurationData request fails', () => {
expect(findFlashError()).toBeNull();
mockRequestCycleAnalyticsData({
mockFetchDurationData: false,
});
wrapper.vm.onGroupSelect(mockData.group);
return waitForPromises().catch(() => {
expect(findFlashError().innerText.trim()).toEqual(
'There was an error while fetching value stream analytics duration data.',
);
});
});
it('will display an error if the fetchStageMedian request fails', () => {
expect(findFlashError()).toBeNull();
......@@ -563,7 +535,7 @@ describe('Cycle Analytics component', () => {
return waitForPromises().catch(() => {
expect(findFlashError().innerText.trim()).toEqual(
'There was an error while fetching value stream analytics duration data.',
'There was an error while fetching value stream analytics data.',
);
});
});
......
import { shallowMount, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import $ from 'jquery';
import 'bootstrap';
import '~/gl_dropdown';
import durationChartStore from 'ee/analytics/cycle_analytics/store/modules/duration_chart';
import Scatterplot from 'ee/analytics/shared/components/scatterplot.vue';
import DurationChart from 'ee/analytics/cycle_analytics/components/duration_chart.vue';
import StageDropdownFilter from 'ee/analytics/cycle_analytics/components/stage_dropdown_filter.vue';
import {
allowedStages as stages,
durationChartPlottableData as scatterData,
durationChartPlottableMedianData as medianLineData,
durationChartPlottableData as durationData,
durationChartPlottableMedianData as durationMedianData,
} from '../mock_data';
function createComponent({ mountFn = shallowMount, props = {}, stubs = {} } = {}) {
const localVue = createLocalVue();
localVue.use(Vuex);
const actionSpies = {
fetchDurationData: jest.fn(),
updateSelectedDurationChartStages: jest.fn(),
};
const fakeStore = ({ initialGetters, initialState }) =>
new Vuex.Store({
modules: {
durationChart: {
...durationChartStore,
getters: {
durationChartPlottableData: () => durationData,
durationChartMedianData: () => durationMedianData,
...initialGetters,
},
state: {
isLoading: false,
...initialState,
},
},
},
});
function createComponent({
mountFn = shallowMount,
stubs = {},
initialState = {},
initialGetters = {},
props = {},
} = {}) {
return mountFn(DurationChart, {
localVue,
store: fakeStore({ initialState, initialGetters }),
propsData: {
isLoading: false,
stages,
scatterData,
medianLineData,
...props,
},
methods: actionSpies,
stubs: {
GlLoadingIcon: true,
Scatterplot: true,
......@@ -82,25 +115,33 @@ describe('DurationChart', () => {
return openStageDropdown(wrapper).then(() => selectStage(wrapper, selectedIndex));
});
it('emits the stageSelected event', () => {
expect(wrapper.emitted().stageSelected).toBeTruthy();
it('calls the `updateSelectedDurationChartStages` action', () => {
expect(actionSpies.updateSelectedDurationChartStages).toHaveBeenCalledWith(selectedStages);
});
});
it('toggles the selected stage', () => {
expect(wrapper.emitted('stageSelected')[0]).toEqual([selectedStages]);
return selectStage(wrapper, selectedIndex).then(() => {
const [updatedStages] = wrapper.emitted('stageSelected')[1];
stages.forEach(stage => {
expect(updatedStages).toContain(stage);
});
describe('with no stages', () => {
beforeEach(() => {
wrapper = createComponent({
mountFn: mount,
props: { stages: [] },
stubs: { StageDropdownFilter: false },
});
});
it('does not render the stage dropdown', () => {
expect(findStageDropdown(wrapper).exists()).toBe(false);
});
});
describe('with no chart data', () => {
beforeEach(() => {
wrapper = createComponent({ props: { scatterData: [], medianLineData: [] } });
wrapper = createComponent({
initialGetters: {
durationChartPlottableData: () => [],
durationChartMedianData: () => [],
},
});
});
it('renders the no data available message', () => {
......@@ -110,11 +151,12 @@ describe('DurationChart', () => {
});
});
describe('while loading', () => {
describe('when isLoading=true', () => {
beforeEach(() => {
wrapper = createComponent({ props: { isLoading: true } });
wrapper = createComponent({ initialState: { isLoading: true } });
});
it('renders loading icon', () => {
it('renders a loader', () => {
expect(findLoader(wrapper).exists()).toBe(true);
});
});
......
......@@ -40,6 +40,8 @@ export const group = {
avatar_url: `${TEST_HOST}/images/home/nasa.svg`,
};
export const selectedGroup = convertObjectPropsToCamelCase(group, { deep: true });
const getStageByTitle = (stages, title) =>
stages.find(stage => stage.title && stage.title.toLowerCase().trim() === title) || {};
......
import * as getters from 'ee/analytics/cycle_analytics/store/getters';
import {
startDate,
endDate,
transformedDurationData,
transformedDurationMedianData,
durationChartPlottableData,
durationChartPlottableMedianData,
allowedStages,
selectedProjects,
} from '../mock_data';
import { startDate, endDate, allowedStages, selectedProjects } from '../mock_data';
let state = null;
......@@ -98,54 +89,6 @@ describe('Cycle analytics getters', () => {
});
});
describe('durationChartPlottableData', () => {
it('returns plottable data for selected stages', () => {
const stateWithDurationData = {
startDate,
endDate,
durationData: transformedDurationData,
};
expect(getters.durationChartPlottableData(stateWithDurationData)).toEqual(
durationChartPlottableData,
);
});
it('returns an empty array if there is no plottable data for the selected stages', () => {
const stateWithDurationData = {
startDate,
endDate,
durationData: [],
};
expect(getters.durationChartPlottableData(stateWithDurationData)).toEqual([]);
});
});
describe('durationChartPlottableMedianData', () => {
it('returns plottable median data for selected stages', () => {
const stateWithDurationMedianData = {
startDate,
endDate,
durationMedianData: transformedDurationMedianData,
};
expect(getters.durationChartMedianData(stateWithDurationMedianData)).toEqual(
durationChartPlottableMedianData,
);
});
it('returns an empty array if there is no plottable median data for the selected stages', () => {
const stateWithDurationMedianData = {
startDate,
endDate,
durationMedianData: [],
};
expect(getters.durationChartMedianData(stateWithDurationMedianData)).toEqual([]);
});
});
const hiddenStage = { ...allowedStages[2], hidden: true };
const givenStages = [allowedStages[0], allowedStages[1], hiddenStage];
describe.each`
......
import * as getters from 'ee/analytics/cycle_analytics/store/modules/duration_chart/getters';
import {
startDate,
endDate,
transformedDurationData,
transformedDurationMedianData,
durationChartPlottableData,
durationChartPlottableMedianData,
} from '../../../mock_data';
const rootState = {
startDate,
endDate,
};
describe('DurationChart getters', () => {
describe('durationChartPlottableData', () => {
it('returns plottable data for selected stages', () => {
const stateWithDurationData = {
durationData: transformedDurationData,
};
expect(getters.durationChartPlottableData(stateWithDurationData, getters, rootState)).toEqual(
durationChartPlottableData,
);
});
it('returns an empty array if there is no plottable data for the selected stages', () => {
const stateWithDurationData = {
durationData: [],
};
expect(getters.durationChartPlottableData(stateWithDurationData, getters, rootState)).toEqual(
[],
);
});
});
describe('durationChartPlottableMedianData', () => {
it('returns plottable median data for selected stages', () => {
const stateWithDurationMedianData = {
durationMedianData: transformedDurationMedianData,
};
expect(
getters.durationChartMedianData(stateWithDurationMedianData, getters, rootState),
).toEqual(durationChartPlottableMedianData);
});
it('returns an empty array if there is no plottable median data for the selected stages', () => {
const stateWithDurationMedianData = {
startDate,
endDate,
durationMedianData: [],
};
expect(
getters.durationChartMedianData(stateWithDurationMedianData, getters, rootState),
).toEqual([]);
});
});
});
import mutations from 'ee/analytics/cycle_analytics/store/modules/duration_chart/mutations';
import * as types from 'ee/analytics/cycle_analytics/store/modules/duration_chart/mutation_types';
import { transformedDurationData, transformedDurationMedianData } from '../../../mock_data';
let state = null;
describe('DurationChart mutations', () => {
beforeEach(() => {
state = {};
});
afterEach(() => {
state = null;
});
it.each`
mutation | stateKey | value
${types.REQUEST_DURATION_DATA} | ${'isLoading'} | ${true}
${types.RECEIVE_DURATION_DATA_ERROR} | ${'isLoading'} | ${false}
`('$mutation will set $stateKey=$value', ({ mutation, stateKey, value }) => {
mutations[mutation](state);
expect(state[stateKey]).toEqual(value);
});
it.each`
mutation | payload | expectedState
${types.UPDATE_SELECTED_DURATION_CHART_STAGES} | ${{ updatedDurationStageData: transformedDurationData, updatedDurationStageMedianData: transformedDurationMedianData }} | ${{ durationData: transformedDurationData, durationMedianData: transformedDurationMedianData }}
`(
'$mutation with payload $payload will update state with $expectedState',
({ mutation, payload, expectedState }) => {
state = {
selectedGroup: { fullPath: 'rad-stage' },
};
mutations[mutation](state, payload);
expect(state).toMatchObject(expectedState);
},
);
describe(`${types.RECEIVE_DURATION_DATA_SUCCESS}`, () => {
it('sets the data correctly and falsifies isLoading', () => {
const stateWithData = {
isLoading: true,
durationData: [['something', 'random']],
};
mutations[types.RECEIVE_DURATION_DATA_SUCCESS](stateWithData, transformedDurationData);
expect(stateWithData.isLoading).toBe(false);
expect(stateWithData.durationData).toBe(transformedDurationData);
});
});
describe(`${types.RECEIVE_DURATION_MEDIAN_DATA_SUCCESS}`, () => {
it('sets the data correctly', () => {
const stateWithData = {
durationMedianData: [['something', 'random']],
};
mutations[types.RECEIVE_DURATION_MEDIAN_DATA_SUCCESS](
stateWithData,
transformedDurationMedianData,
);
expect(stateWithData.durationMedianData).toBe(transformedDurationMedianData);
});
});
describe(`${types.RECEIVE_DURATION_MEDIAN_DATA_ERROR}`, () => {
it('sets durationMedianData to an empty array', () => {
const stateWithData = {
durationMedianData: [['something', 'random']],
};
mutations[types.RECEIVE_DURATION_MEDIAN_DATA_ERROR](stateWithData);
expect(stateWithData.durationMedianData).toStrictEqual([]);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment