Commit 7d2e8db6 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Added specs for getTasksByTypeData

Remove old fixture file
parent 70135cce
...@@ -7,7 +7,6 @@ import { s__, sprintf } from '~/locale'; ...@@ -7,7 +7,6 @@ import { s__, sprintf } from '~/locale';
import { getDateInPast } from '~/lib/utils/datetime_utility'; import { getDateInPast } from '~/lib/utils/datetime_utility';
import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants'; import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { prepareLabelDatasetForChart, generateDatesInRange } from '../utils';
import { PROJECTS_PER_PAGE, DEFAULT_DAYS_IN_PAST } from '../constants'; import { PROJECTS_PER_PAGE, DEFAULT_DAYS_IN_PAST } from '../constants';
import GroupsDropdownFilter from '../../shared/components/groups_dropdown_filter.vue'; import GroupsDropdownFilter from '../../shared/components/groups_dropdown_filter.vue';
import ProjectsDropdownFilter from '../../shared/components/projects_dropdown_filter.vue'; import ProjectsDropdownFilter from '../../shared/components/projects_dropdown_filter.vue';
...@@ -81,10 +80,16 @@ export default { ...@@ -81,10 +80,16 @@ export default {
'errorCode', 'errorCode',
'startDate', 'startDate',
'endDate', 'endDate',
// TODO: remove this
'tasksByType', 'tasksByType',
'medians', 'medians',
]), ]),
...mapGetters(['hasNoAccessError', 'currentGroupPath', 'durationChartPlottableData']), ...mapGetters([
'hasNoAccessError',
'currentGroupPath',
'durationChartPlottableData',
'tasksByTypeChartData',
]),
shouldRenderEmptyState() { shouldRenderEmptyState() {
return !this.selectedGroup; return !this.selectedGroup;
}, },
...@@ -97,6 +102,14 @@ export default { ...@@ -97,6 +102,14 @@ export default {
shouldDisplayDurationChart() { shouldDisplayDurationChart() {
return !this.isLoadingDurationChart && !this.isLoading; return !this.isLoadingDurationChart && !this.isLoading;
}, },
shouldDisplayTasksByTypeChart() {
return (
!this.isLoadingTasksByTypeChart &&
!this.isLoading &&
this.tasksByTypeChartData &&
this.tasksByTypeChartData.seriesData
);
},
dateRange: { dateRange: {
get() { get() {
return { startDate: this.startDate, endDate: this.endDate }; return { startDate: this.startDate, endDate: this.endDate };
...@@ -111,30 +124,6 @@ export default { ...@@ -111,30 +124,6 @@ export default {
hasDateRangeSet() { hasDateRangeSet() {
return this.startDate && this.endDate; return this.startDate && this.endDate;
}, },
typeOfWork() {
// generate settings for the tasksByType chart
// if (!this.hasDateRangeSet) {
// return { option: { legend: false }, datatset: [], range: [] };
// }
// const range = generateDatesInRange(this.startDate, this.endDate).reverse();
// // TODO: diff and data should be replaced with the tasksByTypeData getter
// const diff = range.length + 1;
// const rawData = typeOfWork(diff);
// const { data, seriesNames } = prepareLabelDatasetForChart({
// dataset: Object.values(rawData),
// range,
// });
return {
option: { legend: false },
range: [],
data: [],
seriesNames: [],
};
},
chartDataDescription() { chartDataDescription() {
if (this.selectedGroup) { if (this.selectedGroup) {
const selectedProjectCount = this.setSelectedProjects.length; const selectedProjectCount = this.setSelectedProjects.length;
...@@ -159,7 +148,6 @@ export default { ...@@ -159,7 +148,6 @@ export default {
}, },
}, },
mounted() { mounted() {
// console.log('this.tasksByType', this.tasksByType);
this.initDateRange(); this.initDateRange();
this.setFeatureFlags({ this.setFeatureFlags({
hasDurationChart: this.glFeatures.cycleAnalyticsScatterplotEnabled, hasDurationChart: this.glFeatures.cycleAnalyticsScatterplotEnabled,
...@@ -231,6 +219,9 @@ export default { ...@@ -231,6 +219,9 @@ export default {
with_shared: false, with_shared: false,
order_by: LAST_ACTIVITY_AT, order_by: LAST_ACTIVITY_AT,
}, },
tasksByTypeChartOptions: {
legend: false,
},
}; };
</script> </script>
...@@ -352,32 +343,40 @@ export default { ...@@ -352,32 +343,40 @@ export default {
<gl-loading-icon v-else-if="!isLoading" size="md" class="my-4 py-4" /> <gl-loading-icon v-else-if="!isLoading" size="md" class="my-4 py-4" />
</template> </template>
</div> </div>
<div v-if="hasDateRangeSet"> <template v-if="featureFlags.hasTasksByTypeChart">
<!-- TODO: move into component file --> <div v-if="shouldDisplayTasksByTypeChart">
<div class="row"> <!-- TODO: move into component file -->
<div class="col-12"> <div class="row">
<h2>{{ __('Type of work') }}</h2> <div class="col-12">
<p>{{ __('Showing data for __ groups and __ projects from __ to __') }}</p> <h2>{{ __('Type of work') }}</h2>
<p v-if="tasksByTypeChartData">
{{ __('Showing data for __ groups and __ projects from __ to __') }}
</p>
</div>
</div> </div>
</div> <div v-if="tasksByTypeChartData" class="row">
<div class="row"> <div class="col-12">
<div class="col-12"> <header>
<header> <h3>{{ __('Tasks by type') }}</h3>
<h3>{{ __('Tasks by type') }}</h3> </header>
</header> <!-- TODO: no data available view -->
<section> <section>
<gl-stacked-column-chart <gl-stacked-column-chart
:option="typeOfWork.option" :option="$options.tasksByTypeChartOptions"
:data="typeOfWork.data" :data="tasksByTypeChartData.seriesData"
:group-by="typeOfWork.range" :group-by="tasksByTypeChartData.range"
x-axis-type="category" x-axis-type="category"
x-axis-title="Date" x-axis-title="Date"
y-axis-title="Number of tasks" y-axis-title="Number of tasks"
:series-names="typeOfWork.seriesNames" :series-names="tasksByTypeChartData.seriesNames"
/> />
</section> </section>
</div>
</div>
<div v-else class="bs-callout bs-callout-info">
{{ __('There is no data available. Please change your selection.') }}
</div> </div>
</div> </div>
</div> </template>
</div> </div>
</template> </template>
// TODO: replace this test data with an endpoint
import { __ } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import { toYmd } from '../../shared/utils';
const today = new Date();
const generateRange = (limit = 30) =>
[...Array(limit).keys()]
.map(i => {
const d = getDateInPast(today, i);
return toYmd(new Date(d));
})
.reverse();
function randomInt(range) {
return Math.floor(Math.random() * Math.floor(range));
}
function arrayToObject(arr) {
return arr.reduce((acc, curr) => {
const [key, value] = curr;
return { ...acc, [key]: value };
}, {});
}
const genSeries = dayRange =>
arrayToObject(generateRange(dayRange).map(key => [key, randomInt(100)]));
const generateApiResponse = dayRange =>
convertObjectPropsToCamelCase(
[
{
label: {
id: 1,
title: __('Bug'),
color: '#428BCA',
text_color: '#FFFFFF',
},
series: [genSeries(dayRange)],
},
{
label: {
id: 3,
title: __('Backstage'),
color: '#327BCA',
text_color: '#FFFFFF',
},
series: [genSeries(dayRange)],
},
{
label: {
id: 2,
title: __('Feature'),
color: '#428BCA',
text_color: '#FFFFFF',
},
series: [genSeries(dayRange)],
},
],
{ deep: true },
);
const transformResponseToLabelHash = data =>
data.reduce(
(acc, { label: { id, ...labelRest }, series }) => ({
...acc,
[id]: {
label: { id, ...labelRest },
series,
},
}),
{},
);
export const typeOfWork = dayRange =>
transformResponseToLabelHash(
convertObjectPropsToCamelCase(generateApiResponse(dayRange), { deep: true }),
);
export default {};
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import { dateFormats } from '../../shared/constants'; import { dateFormats } from '../../shared/constants';
import { getDurationChartData } from '../utils'; import { getDurationChartData, getTasksByTypeData } from '../utils';
export const hasNoAccessError = state => state.errorCode === httpStatus.FORBIDDEN; export const hasNoAccessError = state => state.errorCode === httpStatus.FORBIDDEN;
...@@ -25,5 +25,15 @@ export const durationChartPlottableData = state => { ...@@ -25,5 +25,15 @@ export const durationChartPlottableData = state => {
return plottableData.length ? plottableData : null; return plottableData.length ? plottableData : null;
}; };
export const tasksByTypeData = state =>
state.tasksByType && state.tasksByType.data ? state.tasksByType.data : {}; export const tasksByTypeChartData = ({ tasksByType, startDate, endDate }) => {
// TODO: remove this check, return empty data if need be
if (tasksByType && tasksByType.data.length) {
return getTasksByTypeData({
data: tasksByType.data,
startDate,
endDate,
});
}
return {};
};
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { transformRawStages } from '../utils'; import { transformRawStages, transformRawTasksByTypeData } from '../utils';
export default { export default {
[types.SET_FEATURE_FLAGS](state, featureFlags) { [types.SET_FEATURE_FLAGS](state, featureFlags) {
...@@ -142,7 +142,7 @@ export default { ...@@ -142,7 +142,7 @@ export default {
state.isLoadingTasksByTypeChart = false; state.isLoadingTasksByTypeChart = false;
state.tasksByType = { state.tasksByType = {
...state.tasksByType, ...state.tasksByType,
data: convertObjectPropsToCamelCase(data, { deep: true }), data: transformRawTasksByTypeData(data),
}; };
}, },
[types.REQUEST_CREATE_CUSTOM_STAGE](state) { [types.REQUEST_CREATE_CUSTOM_STAGE](state) {
......
...@@ -78,6 +78,28 @@ export const transformRawStages = (stages = []) => ...@@ -78,6 +78,28 @@ export const transformRawStages = (stages = []) =>
name: name.length ? name : title, name: name.length ? name : title,
})); }));
export const arrayToObject = (arr = []) => {
return arr.reduce((acc, curr) => {
const [key, value] = curr;
return { ...acc, [key]: value };
}, {});
};
// converts the series data into key value pairs
export const transformRawTasksByTypeData = (data = []) => {
// TODO: does processing here make sense? if so add specs
if (!data.length) return [];
return data.map(({ series, ...rest }) =>
convertObjectPropsToCamelCase(
{
...rest,
series: arrayToObject(series),
},
{ deep: true },
),
);
};
export const nestQueryStringKeys = (obj = null, targetKey = '') => { export const nestQueryStringKeys = (obj = null, targetKey = '') => {
if (!obj || !isString(targetKey) || !targetKey.length) return {}; if (!obj || !isString(targetKey) || !targetKey.length) return {};
return Object.entries(obj).reduce((prev, [key, value]) => { return Object.entries(obj).reduce((prev, [key, value]) => {
...@@ -191,34 +213,44 @@ export const getDurationChartData = (data, startDate, endDate) => { ...@@ -191,34 +213,44 @@ export const getDurationChartData = (data, startDate, endDate) => {
return eventData; return eventData;
}; };
// takes the type of work data and converts to a k:v structure const toUnix = datetime => new Date(datetime).getTime();
export const transformRawTypeOfWorkData = raw => {}; export const orderByDate = (a, b) => toUnix(a) - toUnix(b);
export const prepareLabelDatasetForChart = ({ dataset, range }) => // TODO: code blocks + specs
dataset.reduce( // The api only returns datapoints with a value, 0 values are ignored
(acc, curr) => { const zeroMissingDataPoints = ({ data, defaultData }) => {
const { // overwrites the default values with any value that was returned from the api
label: { title }, return { ...defaultData, ...data };
series: [datapoints], };
} = curr;
acc.seriesNames = [...acc.seriesNames, title];
acc.data = [...acc.data, range.map(index => (datapoints[index] ? datapoints[index] : 0))];
return acc;
},
{ data: [], seriesNames: [] },
);
export const flattenTaskByTypeSeries = series => // TODO: docblocks
series.map(dataSet => { // Array of values [date, value]
// ignore the date, just return the value // ignore the date, just return the value, default sort by ascending date
return dataSet[1]; export const flattenTaskByTypeSeries = (series = {}) =>
}); Object.entries(series)
.sort((a, b) => orderByDate(a[0], b[0]))
.map(dataSet => dataSet[1]);
// TODO: docblocks // TODO: docblocks
export const getTasksByTypeData = ({ data, startDate, endDate }) => { // GROSS
export const getTasksByTypeData = ({ data = [], startDate = null, endDate = null }) => {
// TODO: check that the date range and datapoint values are in the same order // TODO: check that the date range and datapoint values are in the same order
const range = getDatesInRange(startDate, endDate, toYmd).reverse(); if (!startDate || !endDate || !data.length) {
return {
range: [],
seriesData: [],
seriesNames: [],
};
}
const range = getDatesInRange(startDate, endDate, toYmd).sort(orderByDate);
const defaultData = range.reduce(
(acc, date) => ({
...acc,
[date]: 0,
}),
{},
);
// TODO: handle zero's? // TODO: handle zero's?
// TODO: fixup seeded data so it falls in the correct date range // TODO: fixup seeded data so it falls in the correct date range
...@@ -231,11 +263,13 @@ export const getTasksByTypeData = ({ data, startDate, endDate }) => { ...@@ -231,11 +263,13 @@ export const getTasksByTypeData = ({ data, startDate, endDate }) => {
// TODO: double check if BE fills in all the dates and adds zeros // TODO: double check if BE fills in all the dates and adds zeros
acc.seriesNames = [...acc.seriesNames, title]; acc.seriesNames = [...acc.seriesNames, title];
// TODO: maybe flatmap // TODO: maybe flatmap
acc.data = [...acc.data, flattenTaskByTypeSeries(series)]; // series is already an object at this point
const fullData = zeroMissingDataPoints({ data: series, defaultData });
acc.seriesData = [...acc.seriesData, flattenTaskByTypeSeries(fullData)];
return acc; return acc;
}, },
{ {
data: [], seriesData: [],
seriesNames: [], seriesNames: [],
}, },
); );
......
...@@ -31,6 +31,7 @@ function createComponent({ ...@@ -31,6 +31,7 @@ function createComponent({
withStageSelected = false, withStageSelected = false,
scatterplotEnabled = true, scatterplotEnabled = true,
tasksByTypeChartEnabled = true, tasksByTypeChartEnabled = true,
customizableCycleAnalyticsEnabled = false,
} = {}) { } = {}) {
const func = shallow ? shallowMount : mount; const func = shallow ? shallowMount : mount;
const comp = func(Component, { const comp = func(Component, {
...@@ -46,6 +47,7 @@ function createComponent({ ...@@ -46,6 +47,7 @@ function createComponent({
glFeatures: { glFeatures: {
cycleAnalyticsScatterplotEnabled: scatterplotEnabled, cycleAnalyticsScatterplotEnabled: scatterplotEnabled,
tasksByTypeChart: tasksByTypeChartEnabled, tasksByTypeChart: tasksByTypeChartEnabled,
customizableCycleAnalytics: customizableCycleAnalyticsEnabled,
}, },
}, },
...opts, ...opts,
...@@ -166,7 +168,7 @@ describe('Cycle Analytics component', () => { ...@@ -166,7 +168,7 @@ describe('Cycle Analytics component', () => {
describe('after a filter has been selected', () => { describe('after a filter has been selected', () => {
describe('the user has access to the group', () => { describe('the user has access to the group', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ withStageSelected: true }); wrapper = createComponent({ withStageSelected: true, tasksByTypeChartEnabled: false });
}); });
it('hides the empty state', () => { it('hides the empty state', () => {
...@@ -218,6 +220,7 @@ describe('Cycle Analytics component', () => { ...@@ -218,6 +220,7 @@ describe('Cycle Analytics component', () => {
describe('with durationData', () => { describe('with durationData', () => {
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios);
wrapper.vm.$store.dispatch('setDateRange', { wrapper.vm.$store.dispatch('setDateRange', {
skipFetch: true, skipFetch: true,
startDate: mockData.startDate, startDate: mockData.startDate,
...@@ -248,14 +251,10 @@ describe('Cycle Analytics component', () => { ...@@ -248,14 +251,10 @@ describe('Cycle Analytics component', () => {
}, },
shallow: false, shallow: false,
withStageSelected: true, withStageSelected: true,
tasksByTypeChartEnabled: false,
}); });
}); });
afterEach(() => {
wrapper.destroy();
mock.restore();
});
it('has the first stage selected by default', () => { it('has the first stage selected by default', () => {
const first = selectStageNavItem(0); const first = selectStageNavItem(0);
const second = selectStageNavItem(1); const second = selectStageNavItem(1);
...@@ -281,6 +280,7 @@ describe('Cycle Analytics component', () => { ...@@ -281,6 +280,7 @@ describe('Cycle Analytics component', () => {
describe('the user does not have access to the group', () => { describe('the user does not have access to the group', () => {
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios);
wrapper.vm.$store.dispatch('setSelectedGroup', { wrapper.vm.$store.dispatch('setSelectedGroup', {
...mockData.group, ...mockData.group,
}); });
...@@ -326,14 +326,11 @@ describe('Cycle Analytics component', () => { ...@@ -326,14 +326,11 @@ describe('Cycle Analytics component', () => {
'stage-event-list': true, 'stage-event-list': true,
'stage-nav-item': true, 'stage-nav-item': true,
}, },
provide: {
glFeatures: {
customizableCycleAnalytics: true,
},
},
}, },
shallow: false, shallow: false,
withStageSelected: true, withStageSelected: true,
customizableCycleAnalyticsEnabled: true,
tasksByTypeChartEnabled: false,
}); });
}); });
...@@ -357,6 +354,7 @@ describe('Cycle Analytics component', () => { ...@@ -357,6 +354,7 @@ describe('Cycle Analytics component', () => {
mockFetchStageData = true, mockFetchStageData = true,
mockFetchStageMedian = true, mockFetchStageMedian = true,
mockFetchDurationData = true, mockFetchDurationData = true,
mockFetchTasksByTypeData = true,
}) { }) {
const defaultStatus = 200; const defaultStatus = 200;
const defaultRequests = { const defaultRequests = {
...@@ -375,14 +373,15 @@ describe('Cycle Analytics component', () => { ...@@ -375,14 +373,15 @@ describe('Cycle Analytics component', () => {
endpoint: `/groups/${groupId}/-/labels`, endpoint: `/groups/${groupId}/-/labels`,
response: [...mockData.groupLabels], response: [...mockData.groupLabels],
}, },
fetchTasksByTypeData: {
status: defaultStatus,
endpoint: '/-/analytics/type_of_work/tasks_by_type',
response: { ...mockData.tasksByTypeData },
},
...overrides, ...overrides,
}; };
if (mockFetchTasksByTypeData) {
mock
.onGet(/analytics\/type_of_work\/tasks_by_type/)
.reply(defaultStatus, { ...mockData.tasksByTypeData });
}
if (mockFetchDurationData) { if (mockFetchDurationData) {
mock mock
.onGet(/analytics\/cycle_analytics\/stages\/\d+\/duration_chart/) .onGet(/analytics\/cycle_analytics\/stages\/\d+\/duration_chart/)
...@@ -491,15 +490,7 @@ describe('Cycle Analytics component', () => { ...@@ -491,15 +490,7 @@ describe('Cycle Analytics component', () => {
it('will display an error if the fetchTasksByTypeData request fails', () => { it('will display an error if the fetchTasksByTypeData request fails', () => {
expect(findFlashError()).toBeNull(); expect(findFlashError()).toBeNull();
mockRequestCycleAnalyticsData({ mockRequestCycleAnalyticsData({ mockFetchTasksByTypeData: false });
overrides: {
fetchTasksByTypeData: {
endPoint: '/-/analytics/type_of_work/tasks_by_type',
status: httpStatusCodes.BAD_REQUEST,
response: { response: { status: httpStatusCodes.BAD_REQUEST } },
},
},
});
return selectGroupAndFindError( return selectGroupAndFindError(
'There was an error fetching data for the tasks by type chart', 'There was an error fetching data for the tasks by type chart',
......
...@@ -7,6 +7,7 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; ...@@ -7,6 +7,7 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getDateInPast, getDatesInRange } from '~/lib/utils/datetime_utility'; import { getDateInPast, getDatesInRange } from '~/lib/utils/datetime_utility';
import { mockLabels } from '../../../../../spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data'; import { mockLabels } from '../../../../../spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data';
import { toYmd } from 'ee/analytics/shared/utils'; import { toYmd } from 'ee/analytics/shared/utils';
import { transformRawTasksByTypeData } from 'ee/analytics/cycle_analytics/utils';
const endpoints = { const endpoints = {
customizableCycleAnalyticsStagesAndEvents: 'analytics/cycle_analytics/stages.json', // customizable stages and events endpoint customizableCycleAnalyticsStagesAndEvents: 'analytics/cycle_analytics/stages.json', // customizable stages and events endpoint
...@@ -70,8 +71,7 @@ export const stageMedians = defaultStages.reduce((acc, stage) => { ...@@ -70,8 +71,7 @@ export const stageMedians = defaultStages.reduce((acc, stage) => {
}, {}); }, {});
export const endDate = new Date(2019, 0, 14); export const endDate = new Date(2019, 0, 14);
// Limit to just 5 days data for testing export const startDate = getDateInPast(endDate, DEFAULT_DAYS_IN_PAST);
export const startDate = getDateInPast(endDate, 4);
export const rawIssueEvents = stageFixtures.issue; export const rawIssueEvents = stageFixtures.issue;
export const issueEvents = deepCamelCase(stageFixtures.issue); export const issueEvents = deepCamelCase(stageFixtures.issue);
...@@ -125,8 +125,8 @@ export const customStageEvents = [ ...@@ -125,8 +125,8 @@ export const customStageEvents = [
const dateRange = getDatesInRange(startDate, endDate, toYmd); const dateRange = getDatesInRange(startDate, endDate, toYmd);
export const tasksByTypeData = convertObjectPropsToCamelCase( export const tasksByTypeData = getJSONFixture('analytics/type_of_work/tasks_by_type.json').map(
getJSONFixture('analytics/type_of_work/tasks_by_type.json').map(labelData => { labelData => {
// add data points for our mock date range // add data points for our mock date range
const maxValue = 10; const maxValue = 10;
const series = dateRange.map(date => [date, Math.floor(Math.random() * Math.floor(maxValue))]); const series = dateRange.map(date => [date, Math.floor(Math.random() * Math.floor(maxValue))]);
...@@ -134,12 +134,11 @@ export const tasksByTypeData = convertObjectPropsToCamelCase( ...@@ -134,12 +134,11 @@ export const tasksByTypeData = convertObjectPropsToCamelCase(
...labelData, ...labelData,
series, series,
}; };
}),
{
deep: true,
}, },
); );
export const transformedTasksByTypeData = transformRawTasksByTypeData(tasksByTypeData);
export const rawDurationData = [ export const rawDurationData = [
{ {
duration_in_seconds: 1234000, duration_in_seconds: 1234000,
......
...@@ -18,6 +18,7 @@ import { ...@@ -18,6 +18,7 @@ import {
customizableStagesAndEvents, customizableStagesAndEvents,
tasksByTypeData, tasksByTypeData,
transformedDurationData, transformedDurationData,
transformedTasksByTypeData,
} from '../mock_data'; } from '../mock_data';
let state = null; let state = null;
...@@ -32,34 +33,34 @@ describe('Cycle analytics mutations', () => { ...@@ -32,34 +33,34 @@ describe('Cycle analytics mutations', () => {
}); });
it.each` it.each`
mutation | stateKey | value mutation | stateKey | value
${types.HIDE_CUSTOM_STAGE_FORM} | ${'isCreatingCustomStage'} | ${false} ${types.HIDE_CUSTOM_STAGE_FORM} | ${'isCreatingCustomStage'} | ${false}
${types.SHOW_CUSTOM_STAGE_FORM} | ${'isCreatingCustomStage'} | ${true} ${types.SHOW_CUSTOM_STAGE_FORM} | ${'isCreatingCustomStage'} | ${true}
${types.EDIT_CUSTOM_STAGE} | ${'isEditingCustomStage'} | ${true} ${types.EDIT_CUSTOM_STAGE} | ${'isEditingCustomStage'} | ${true}
${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true} ${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true} ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false} ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false}
${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'isLoading'} | ${true} ${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'isLoading'} | ${true}
${types.REQUEST_GROUP_LABELS} | ${'labels'} | ${[]} ${types.REQUEST_GROUP_LABELS} | ${'labels'} | ${[]}
${types.RECEIVE_GROUP_LABELS_ERROR} | ${'labels'} | ${[]} ${types.RECEIVE_GROUP_LABELS_ERROR} | ${'labels'} | ${[]}
${types.RECEIVE_SUMMARY_DATA_ERROR} | ${'summary'} | ${[]} ${types.RECEIVE_SUMMARY_DATA_ERROR} | ${'summary'} | ${[]}
${types.REQUEST_SUMMARY_DATA} | ${'summary'} | ${[]} ${types.REQUEST_SUMMARY_DATA} | ${'summary'} | ${[]}
${types.RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR} | ${'stages'} | ${[]} ${types.RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR} | ${'stages'} | ${[]}
${types.REQUEST_GROUP_STAGES_AND_EVENTS} | ${'stages'} | ${[]} ${types.REQUEST_GROUP_STAGES_AND_EVENTS} | ${'stages'} | ${[]}
${types.RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR} | ${'customStageFormEvents'} | ${[]} ${types.RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR} | ${'customStageFormEvents'} | ${[]}
${types.REQUEST_GROUP_STAGES_AND_EVENTS} | ${'customStageFormEvents'} | ${[]} ${types.REQUEST_GROUP_STAGES_AND_EVENTS} | ${'customStageFormEvents'} | ${[]}
${types.REQUEST_CREATE_CUSTOM_STAGE} | ${'isSavingCustomStage'} | ${true} ${types.REQUEST_CREATE_CUSTOM_STAGE} | ${'isSavingCustomStage'} | ${true}
${types.RECEIVE_CREATE_CUSTOM_STAGE_RESPONSE} | ${'isSavingCustomStage'} | ${false} ${types.RECEIVE_CREATE_CUSTOM_STAGE_RESPONSE} | ${'isSavingCustomStage'} | ${false}
${types.REQUEST_TASKS_BY_TYPE_DATA} | ${'isLoadingChartData'} | ${true} ${types.REQUEST_TASKS_BY_TYPE_DATA} | ${'isLoadingTasksByTypeChart'} | ${true}
${types.RECEIVE_TASKS_BY_TYPE_DATA_ERROR} | ${'isLoadingChartData'} | ${false} ${types.RECEIVE_TASKS_BY_TYPE_DATA_ERROR} | ${'isLoadingTasksByTypeChart'} | ${false}
${types.REQUEST_UPDATE_STAGE} | ${'isLoading'} | ${true} ${types.REQUEST_UPDATE_STAGE} | ${'isLoading'} | ${true}
${types.RECEIVE_UPDATE_STAGE_RESPONSE} | ${'isLoading'} | ${false} ${types.RECEIVE_UPDATE_STAGE_RESPONSE} | ${'isLoading'} | ${false}
${types.REQUEST_REMOVE_STAGE} | ${'isLoading'} | ${true} ${types.REQUEST_REMOVE_STAGE} | ${'isLoading'} | ${true}
${types.RECEIVE_REMOVE_STAGE_RESPONSE} | ${'isLoading'} | ${false} ${types.RECEIVE_REMOVE_STAGE_RESPONSE} | ${'isLoading'} | ${false}
${types.REQUEST_DURATION_DATA} | ${'isLoadingDurationChart'} | ${true} ${types.REQUEST_DURATION_DATA} | ${'isLoadingDurationChart'} | ${true}
${types.RECEIVE_DURATION_DATA_ERROR} | ${'isLoadingDurationChart'} | ${false} ${types.RECEIVE_DURATION_DATA_ERROR} | ${'isLoadingDurationChart'} | ${false}
${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}} ${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}}
${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}} ${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}}
`('$mutation will set $stateKey=$value', ({ mutation, stateKey, value }) => { `('$mutation will set $stateKey=$value', ({ mutation, stateKey, value }) => {
mutations[mutation](state); mutations[mutation](state);
...@@ -189,17 +190,17 @@ describe('Cycle analytics mutations', () => { ...@@ -189,17 +190,17 @@ describe('Cycle analytics mutations', () => {
}); });
describe(`${types.RECEIVE_TASKS_BY_TYPE_DATA_SUCCESS}`, () => { describe(`${types.RECEIVE_TASKS_BY_TYPE_DATA_SUCCESS}`, () => {
it('sets isLoadingChartData to false', () => { it('sets isLoadingTasksByTypeChart to false', () => {
mutations[types.RECEIVE_TASKS_BY_TYPE_DATA_SUCCESS](state, {}); mutations[types.RECEIVE_TASKS_BY_TYPE_DATA_SUCCESS](state, {});
expect(state.isLoadingChartData).toEqual(false); expect(state.isLoadingTasksByTypeChart).toEqual(false);
}); });
it('sets tasksByType.data to the raw returned chart data', () => { it('sets tasksByType.data to the raw returned chart data', () => {
state = { tasksByType: { data: null } }; state = { tasksByType: { data: null } };
mutations[types.RECEIVE_TASKS_BY_TYPE_DATA_SUCCESS](state, tasksByTypeData); mutations[types.RECEIVE_TASKS_BY_TYPE_DATA_SUCCESS](state, tasksByTypeData);
expect(state.tasksByType.data).toEqual(tasksByTypeData); expect(state.tasksByType.data).toEqual(transformedTasksByTypeData);
}); });
}); });
......
import { isNumber } from 'underscore';
import { getDatesInRange } from '~/lib/utils/datetime_utility';
import { import {
isStartEvent, isStartEvent,
isLabelEvent, isLabelEvent,
...@@ -12,7 +14,10 @@ import { ...@@ -12,7 +14,10 @@ import {
isPersistedStage, isPersistedStage,
getTasksByTypeData, getTasksByTypeData,
flattenTaskByTypeSeries, flattenTaskByTypeSeries,
orderByDate,
arrayToObject, // TODO: dedupe this?
} from 'ee/analytics/cycle_analytics/utils'; } from 'ee/analytics/cycle_analytics/utils';
import { toYmd } from 'ee/analytics/shared/utils';
import { import {
customStageEvents as events, customStageEvents as events,
labelStartEvent, labelStartEvent,
...@@ -26,6 +31,7 @@ import { ...@@ -26,6 +31,7 @@ import {
issueStage, issueStage,
rawCustomStage, rawCustomStage,
tasksByTypeData, tasksByTypeData,
transformedTasksByTypeData,
} from './mock_data'; } from './mock_data';
const labelEvents = [labelStartEvent, labelStopEvent].map(i => i.identifier); const labelEvents = [labelStartEvent, labelStopEvent].map(i => i.identifier);
...@@ -203,67 +209,106 @@ describe('Cycle analytics utils', () => { ...@@ -203,67 +209,106 @@ describe('Cycle analytics utils', () => {
}); });
}); });
describe.skip('flattenTaskByTypeSeries', () => {}); describe('flattenTaskByTypeSeries', () => {
const dummySeries = arrayToObject([
['2019-01-16', 40],
['2019-01-14', 20],
['2019-01-12', 10],
['2019-01-15', 30],
]);
describe.only('getTasksByTypeData', () => { let transformedDummySeries = [];
let transformed = {};
const rawData = tasksByTypeData; beforeEach(() => {
const labels = rawData.map(d => { transformedDummySeries = flattenTaskByTypeSeries(dummySeries);
const { label } = d;
return label.title;
}); });
const data = rawData.map(d => { it('extracts the value from an array of datetime / value pairs', () => {
const { series } = d; expect(transformedDummySeries.every(isNumber)).toEqual(true);
return flattenTaskByTypeSeries(series); Object.values(dummySeries).forEach(v => {
expect(transformedDummySeries.includes(v)).toBeTruthy();
});
}); });
const range = []; it('sorts the items by the datetime parameter', () => {
console.log('rawData', rawData); expect(transformedDummySeries).toEqual([10, 20, 30, 40]);
// console.log('labels', labels); });
console.log('data', data); });
beforeEach(() => { describe('orderByDate', () => {
transformed = getTasksByTypeData({ data: rawData, startDate, endDate }); it('sorts dates from the earliest to latest', () => {
expect(['2019-01-14', '2019-01-12', '2019-01-16', '2019-01-15'].sort(orderByDate)).toEqual([
'2019-01-12',
'2019-01-14',
'2019-01-15',
'2019-01-16',
]);
}); });
});
it('will return an object with the properties needed for the chart', () => { describe('getTasksByTypeData', () => {
['seriesNames', 'data', 'range'].forEach(key => { let transformed = {};
expect(transformed).toHaveProperty(key);
}); const range = getDatesInRange(startDate, endDate, toYmd);
const seriesData = transformedTasksByTypeData.map(({ series }) => Object.values(series));
const labels = transformedTasksByTypeData.map(d => {
const { label } = d;
return label.title;
}); });
describe('seriesNames', () => { it('will return blank arrays if given no data', () => {
it('returns the names of all the labels in the dataset', () => { [{ data: [], startDate, endDate }, [], {}].forEach(chartData => {
expect(transformed.seriesNames).toEqual(labels); transformed = getTasksByTypeData(chartData);
['seriesNames', 'seriesData', 'range'].forEach(key => {
expect(transformed[key]).toEqual([]);
});
}); });
}); });
describe('range', () => { describe('with data', () => {
it('returns the date range as an array', () => { beforeEach(() => {
expect(transformed.range).toEqual(range); transformed = getTasksByTypeData({ data: transformedTasksByTypeData, startDate, endDate });
});
it('includes each day between the start date and end date', () => {
expect(transformed.range).toEqual(range);
}); });
it('includes the start date and end date', () => {
expect(transformed.range).toContain(startDate); it('will return an object with the properties needed for the chart', () => {
expect(transformed.range).toContain(endDate); ['seriesNames', 'seriesData', 'range'].forEach(key => {
expect(transformed).toHaveProperty(key);
});
}); });
});
describe('data', () => { describe('seriesNames', () => {
it('returns an array of data points', () => { it('returns the names of all the labels in the dataset', () => {
expect(transformed.data).toEqual(data); expect(transformed.seriesNames).toEqual(labels);
});
}); });
it('contains an array of data for each label', () => { describe('range', () => {
expect(transformed.data.length).toEqual(labels.length); it('returns the date range as an array', () => {
expect(transformed.range).toEqual(range);
});
it('the start date is the first element', () => {
expect(transformed.range[0]).toEqual(toYmd(startDate));
});
it('the end date is the last element', () => {
expect(transformed.range[transformed.range.length - 1]).toEqual(toYmd(endDate));
});
}); });
it('contains a value for each day in the range', () => { describe('data', () => {
transformed.data.forEach(d => { it('returns an array of data points', () => {
expect(d.length).toEqual(transformed.range.length); expect(transformed.seriesData).toEqual(seriesData);
});
it('contains an array of data for each label', () => {
expect(transformed.seriesData.length).toEqual(labels.length);
});
it('contains a value for each day in the range', () => {
transformed.seriesData.forEach(d => {
expect(d.length).toEqual(transformed.range.length);
});
}); });
}); });
}); });
......
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