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