Commit 789d9719 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Added jest specs for tasks_by_type_chart

Minor cleanup and added snapshot tests and
tests with and without the tasksByTypeChart
feature flag
parent a9720f23
...@@ -79,7 +79,6 @@ export default { ...@@ -79,7 +79,6 @@ export default {
'errorCode', 'errorCode',
'startDate', 'startDate',
'endDate', 'endDate',
// TODO: remove this
'tasksByType', 'tasksByType',
'medians', 'medians',
]), ]),
...@@ -101,9 +100,7 @@ export default { ...@@ -101,9 +100,7 @@ export default {
shouldDisplayDurationChart() { shouldDisplayDurationChart() {
return !this.isLoadingDurationChart && !this.isLoading; return !this.isLoadingDurationChart && !this.isLoading;
}, },
shouldDisplayTasksByTypeChart() {
return !this.isLoadingTasksByTypeChart && !this.isLoading && this.tasksByTypeChartData;
},
dateRange: { dateRange: {
get() { get() {
return { startDate: this.startDate, endDate: this.endDate }; return { startDate: this.startDate, endDate: this.endDate };
...@@ -142,9 +139,6 @@ export default { ...@@ -142,9 +139,6 @@ export default {
hasDurationChart: this.glFeatures.cycleAnalyticsScatterplotEnabled, hasDurationChart: this.glFeatures.cycleAnalyticsScatterplotEnabled,
hasTasksByTypeChart: this.glFeatures.tasksByTypeChart, hasTasksByTypeChart: this.glFeatures.tasksByTypeChart,
}); });
console.log('mounted::this.glFeatures', this.glFeatures);
console.log('mounted::this.featureFlags', this.featureFlags);
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -332,27 +326,17 @@ export default { ...@@ -332,27 +326,17 @@ 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>
<template v-if="featureFlags.hasTasksByTypeChart"> <template v-if="featureFlags.hasTasksByTypeChart">
<div v-if="!isLoading"> <div class="tasks-by-type-chart">
<gl-loading-icon v-if="isLoadingTasksByTypeChart" size="md" class="my-4 py-4" /> <gl-loading-icon
<div v-else class="tasks-by-type-chart"> v-if="isLoading || isLoadingTasksByTypeChart"
size="md"
class="my-4 py-4"
/>
<div v-else>
<tasks-by-type-chart <tasks-by-type-chart
v-if="shouldDisplayTasksByTypeChart" :chart-data="tasksByTypeChartData"
:data="tasksByTypeChartData.seriesData"
:group-by="tasksByTypeChartData.range"
:series-names="tasksByTypeChartData.seriesNames"
:filters="selectedTasksByTypeFilters" :filters="selectedTasksByTypeFilters"
/> />
<div v-else>
<!-- TODO: move this inside the component -->
<div class="row">
<div class="col-12">
<h3>{{ __('Type of work') }}</h3>
<div class="bs-callout bs-callout-info">
<p>{{ __('There is no data available. Please change your selection.') }}</p>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
......
...@@ -16,22 +16,14 @@ export default { ...@@ -16,22 +16,14 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
data: { chartData: {
type: Array, type: Object,
required: true,
},
groupBy: {
type: Array,
required: true,
},
seriesNames: {
type: Array,
required: true, required: true,
}, },
}, },
computed: { computed: {
hasData() { hasData() {
return this.data && this.data.length; return this.chartData && this.chartData.data && this.chartData.data.length;
}, },
selectedFiltersText() { selectedFiltersText() {
// TODO: I think we should show labels that have 0 data, currently doesnt appear // TODO: I think we should show labels that have 0 data, currently doesnt appear
...@@ -93,15 +85,22 @@ export default { ...@@ -93,15 +85,22 @@ export default {
<section> <section>
<gl-stacked-column-chart <gl-stacked-column-chart
:option="$options.chartOptions" :option="$options.chartOptions"
:data="data" :data="chartData.data"
:group-by="groupBy" :group-by="chartData.groupBy"
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="seriesNames" :series-names="chartData.seriesNames"
/> />
</section> </section>
</div> </div>
</div> </div>
<div v-else class="row">
<div class="col-12">
<div class="bs-callout bs-callout-info">
<p>{{ __('There is no data available. Please change your selection.') }}</p>
</div>
</div>
</div>
</div> </div>
</template> </template>
...@@ -131,8 +131,6 @@ export const editCustomStage = ({ commit, dispatch }, selectedStage = {}) => { ...@@ -131,8 +131,6 @@ export const editCustomStage = ({ commit, dispatch }, selectedStage = {}) => {
export const requestSummaryData = ({ commit }) => commit(types.REQUEST_SUMMARY_DATA); export const requestSummaryData = ({ commit }) => commit(types.REQUEST_SUMMARY_DATA);
export const receiveSummaryDataError = ({ commit }, error) => { export const receiveSummaryDataError = ({ commit }, error) => {
console.log('receiveSummaryDataError::error', error);
commit(types.RECEIVE_SUMMARY_DATA_ERROR, error); commit(types.RECEIVE_SUMMARY_DATA_ERROR, error);
createFlash(__('There was an error while fetching cycle analytics summary data.')); createFlash(__('There was an error while fetching cycle analytics summary data.'));
}; };
...@@ -249,15 +247,14 @@ export const createCustomStage = ({ dispatch, state }, data) => { ...@@ -249,15 +247,14 @@ export const createCustomStage = ({ dispatch, state }, data) => {
}; };
export const receiveTasksByTypeDataSuccess = ({ commit }, data) => { export const receiveTasksByTypeDataSuccess = ({ commit }, data) => {
console.log('receiveTasksByTypeDataSuccess::data', data);
commit(types.RECEIVE_TASKS_BY_TYPE_DATA_SUCCESS, data); commit(types.RECEIVE_TASKS_BY_TYPE_DATA_SUCCESS, data);
}; };
export const receiveTasksByTypeDataError = ({ commit }, error) => { export const receiveTasksByTypeDataError = ({ commit }, error) => {
console.log('receiveTasksByTypeDataError::error', error);
commit(types.RECEIVE_TASKS_BY_TYPE_DATA_ERROR, error); commit(types.RECEIVE_TASKS_BY_TYPE_DATA_ERROR, error);
createFlash(__('There was an error fetching data for the tasks by type chart')); createFlash(__('There was an error fetching data for the tasks by type chart'));
}; };
export const requestTasksByTypeData = ({ commit }) => commit(types.REQUEST_TASKS_BY_TYPE_DATA); export const requestTasksByTypeData = ({ commit }) => commit(types.REQUEST_TASKS_BY_TYPE_DATA);
export const fetchTasksByTypeData = ({ dispatch, state, getters }) => { export const fetchTasksByTypeData = ({ dispatch, state, getters }) => {
......
...@@ -35,5 +35,5 @@ export const tasksByTypeChartData = ({ tasksByType, startDate, endDate }) => { ...@@ -35,5 +35,5 @@ export const tasksByTypeChartData = ({ tasksByType, startDate, endDate }) => {
endDate, endDate,
}); });
} }
return null; return { groupBy: [], data: [], seriesNames: [] };
}; };
...@@ -78,21 +78,14 @@ export const transformRawStages = (stages = []) => ...@@ -78,21 +78,14 @@ export const transformRawStages = (stages = []) =>
name: name.length ? name : title, name: name.length ? name : title,
})); }));
export const arrayToObject = (arr = []) =>
arr.reduce((acc, curr) => {
const [key, value] = curr;
return { ...acc, [key]: value };
}, {});
// converts the series data into key value pairs // converts the series data into key value pairs
export const transformRawTasksByTypeData = (data = []) => { export const transformRawTasksByTypeData = (data = []) => {
// TODO: does processing here make sense? if so add specs
if (!data.length) return []; if (!data.length) return [];
return data.map(({ series, ...rest }) => return data.map(({ series, ...rest }) =>
convertObjectPropsToCamelCase( convertObjectPropsToCamelCase(
{ {
...rest, ...rest,
series: arrayToObject(series), series: Object.fromEntries(series),
}, },
{ deep: true }, { deep: true },
), ),
...@@ -217,10 +210,8 @@ export const orderByDate = (a, b) => toUnix(a) - toUnix(b); ...@@ -217,10 +210,8 @@ export const orderByDate = (a, b) => toUnix(a) - toUnix(b);
// TODO: code blocks + specs // TODO: code blocks + specs
// The api only returns datapoints with a value, 0 values are ignored // 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
// overwrites the default values with any value that was returned from the api const zeroMissingDataPoints = ({ data, defaultData }) => ({ ...defaultData, ...data });
return { ...defaultData, ...data };
};
// TODO: docblocks // TODO: docblocks
// Array of values [date, value] // Array of values [date, value]
...@@ -233,25 +224,22 @@ export const flattenTaskByTypeSeries = (series = {}) => ...@@ -233,25 +224,22 @@ export const flattenTaskByTypeSeries = (series = {}) =>
// TODO: docblocks // TODO: docblocks
// GROSS // GROSS
export const getTasksByTypeData = ({ data = [], startDate = null, endDate = null }) => { export const getTasksByTypeData = ({ data = [], startDate = null, endDate = null }) => {
// TODO: check that the date range and datapoint values are in the same order
if (!startDate || !endDate || !data.length) { if (!startDate || !endDate || !data.length) {
return { return {
range: [], groupBy: [],
seriesData: [], data: [],
seriesNames: [], seriesNames: [],
}; };
} }
const range = getDatesInRange(startDate, endDate, toYmd).sort(orderByDate); const groupBy = getDatesInRange(startDate, endDate, toYmd).sort(orderByDate);
const defaultData = range.reduce( const zeroValuesForEachDataPoint = groupBy.reduce(
(acc, date) => ({ (acc, date) => ({
...acc, ...acc,
[date]: 0, [date]: 0,
}), }),
{}, {},
); );
// TODO: handle zero's?
// TODO: fixup seeded data so it falls in the correct date range
const transformed = data.reduce( const transformed = data.reduce(
(acc, curr) => { (acc, curr) => {
...@@ -259,22 +247,22 @@ export const getTasksByTypeData = ({ data = [], startDate = null, endDate = null ...@@ -259,22 +247,22 @@ export const getTasksByTypeData = ({ data = [], startDate = null, endDate = null
label: { title }, label: { title },
series, series,
} = curr; } = curr;
// 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 acc.data = [
// series is already an object at this point ...acc.data,
const fullData = zeroMissingDataPoints({ data: series, defaultData }); // adds 0 values for each data point and overrides with data from the series
acc.seriesData = [...acc.seriesData, flattenTaskByTypeSeries(fullData)]; flattenTaskByTypeSeries({ ...zeroValuesForEachDataPoint, ...series }),
];
return acc; return acc;
}, },
{ {
seriesData: [], data: [],
seriesNames: [], seriesNames: [],
}, },
); );
return { return {
...transformed, ...transformed,
range, groupBy,
}; };
}; };
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TasksByTypeChart no data available should render the no data available message 1`] = `
"<div>
<div class=\\"row\\">
<div class=\\"col-12\\">
<h3>Type of work</h3>
<!---->
</div>
</div>
<div class=\\"row\\">
<div class=\\"col-12\\">
<div class=\\"bs-callout bs-callout-info\\">
<p>There is no data available. Please change your selection.</p>
</div>
</div>
</div>
</div>"
`;
exports[`TasksByTypeChart with data available should render the loading chart 1`] = `
"<div>
<div class=\\"row\\">
<div class=\\"col-12\\">
<h3>Type of work</h3>
<p>
Showing data for group 'Gitlab Org' from Dec 11, 2019 to Jan 10, 2020
</p>
</div>
</div>
<div class=\\"row\\">
<div class=\\"col-12\\">
<header>
<h4>Tasks by type</h4>
<p>Showing Issue and 3 labels</p>
</header>
<section>
<gl-stacked-column-chart-stub data=\\"0,1,2,5,2,3,2,4,1\\" option=\\"[object Object]\\" presentation=\\"stacked\\" groupby=\\"Group 1,Group 2,Group 3\\" xaxistype=\\"category\\" xaxistitle=\\"Date\\" yaxistitle=\\"Number of tasks\\" seriesnames=\\"Cool label,Normal label\\" legendaveragetext=\\"Avg\\" legendmaxtext=\\"Max\\"></gl-stacked-column-chart-stub>
</section>
</div>
</div>
</div>"
`;
...@@ -25,6 +25,13 @@ const baseStagesEndpoint = '/-/analytics/cycle_analytics/stages'; ...@@ -25,6 +25,13 @@ const baseStagesEndpoint = '/-/analytics/cycle_analytics/stages';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
const defaultStubs = {
'summary-table': true,
'stage-event-list': true,
'stage-nav-item': true,
'tasks-by-type-chart': true,
};
function createComponent({ function createComponent({
opts = {}, opts = {},
shallow = true, shallow = true,
...@@ -321,11 +328,7 @@ describe('Cycle Analytics component', () => { ...@@ -321,11 +328,7 @@ describe('Cycle Analytics component', () => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
wrapper = createComponent({ wrapper = createComponent({
opts: { opts: {
stubs: { stubs: defaultStubs,
'summary-table': true,
'stage-event-list': true,
'stage-nav-item': true,
},
}, },
shallow: false, shallow: false,
withStageSelected: true, withStageSelected: true,
...@@ -343,6 +346,53 @@ describe('Cycle Analytics component', () => { ...@@ -343,6 +346,53 @@ describe('Cycle Analytics component', () => {
expect(wrapper.find('.js-add-stage-button').exists()).toBe(true); expect(wrapper.find('.js-add-stage-button').exists()).toBe(true);
}); });
}); });
describe('with tasksByTypeChart=true', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
wrapper = createComponent({
opts: {
stubs: defaultStubs,
},
shallow: false,
withStageSelected: true,
customizableCycleAnalyticsEnabled: false,
tasksByTypeChartEnabled: true,
});
});
afterEach(() => {
wrapper.destroy();
mock.restore();
});
it('displays the tasks by type chart', () => {
expect(wrapper.find('.tasks-by-type-chart').exists()).toBe(true);
});
});
describe('with tasksByTypeChart=false', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
wrapper = createComponent({
opts: {
stubs: defaultStubs,
},
shallow: false,
withStageSelected: true,
customizableCycleAnalyticsEnabled: false,
tasksByTypeChartEnabled: false,
});
});
afterEach(() => {
wrapper.destroy();
mock.restore();
});
it('does not render the tasks by type chart', () => {
expect(wrapper.find('.tasks-by-type-chart').exists()).toBe(false);
});
});
}); });
}); });
......
import { shallowMount } from '@vue/test-utils';
import TasksByTypeChart from 'ee/analytics/cycle_analytics/components/tasks_by_type_chart.vue';
import { TASKS_BY_TYPE_SUBJECT_ISSUE } from 'ee/analytics/cycle_analytics/constants';
// TODO: maybe move to mock data
const seriesNames = ['Cool label', 'Normal label'];
const data = [[0, 1, 2], [5, 2, 3], [2, 4, 1]];
const groupBy = ['Group 1', 'Group 2', 'Group 3'];
const filters = {
selectedGroup: {
id: 22,
name: 'Gitlab Org',
fullName: 'Gitlab Org',
fullPath: 'gitlab-org',
},
selectedProjectIds: [],
startDate: new Date('2019-12-11'),
endDate: new Date('2020-01-10'),
subject: TASKS_BY_TYPE_SUBJECT_ISSUE,
selectedLabelIds: [1, 2, 3],
};
describe('TasksByTypeChart', () => {
function createComponent(props) {
return shallowMount(TasksByTypeChart, {
propsData: {
filters,
chartData: {
groupBy,
data,
seriesNames,
},
...props,
},
});
}
let wrapper = null;
afterEach(() => {
wrapper.destroy();
});
describe('with data available', () => {
beforeEach(() => {
wrapper = createComponent({});
});
it('should render the loading chart', () => {
expect(wrapper.html()).toMatchSnapshot();
});
});
describe('no data available', () => {
beforeEach(() => {
wrapper = createComponent({
chartData: {
groupBy: [],
data: [],
seriesNames: [],
},
});
});
it('should render the no data available message', () => {
expect(wrapper.html()).toMatchSnapshot();
});
});
});
...@@ -15,7 +15,6 @@ import { ...@@ -15,7 +15,6 @@ import {
getTasksByTypeData, getTasksByTypeData,
flattenTaskByTypeSeries, flattenTaskByTypeSeries,
orderByDate, 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 { toYmd } from 'ee/analytics/shared/utils';
import { import {
...@@ -209,7 +208,7 @@ describe('Cycle analytics utils', () => { ...@@ -209,7 +208,7 @@ describe('Cycle analytics utils', () => {
}); });
describe('flattenTaskByTypeSeries', () => { describe('flattenTaskByTypeSeries', () => {
const dummySeries = arrayToObject([ const dummySeries = Object.fromEntries([
['2019-01-16', 40], ['2019-01-16', 40],
['2019-01-14', 20], ['2019-01-14', 20],
['2019-01-12', 10], ['2019-01-12', 10],
...@@ -248,8 +247,8 @@ describe('Cycle analytics utils', () => { ...@@ -248,8 +247,8 @@ describe('Cycle analytics utils', () => {
describe('getTasksByTypeData', () => { describe('getTasksByTypeData', () => {
let transformed = {}; let transformed = {};
const range = getDatesInRange(startDate, endDate, toYmd); const groupBy = getDatesInRange(startDate, endDate, toYmd);
const seriesData = transformedTasksByTypeData.map(({ series }) => Object.values(series)); const data = transformedTasksByTypeData.map(({ series }) => Object.values(series));
const labels = transformedTasksByTypeData.map(d => { const labels = transformedTasksByTypeData.map(d => {
const { label } = d; const { label } = d;
return label.title; return label.title;
...@@ -258,7 +257,7 @@ describe('Cycle analytics utils', () => { ...@@ -258,7 +257,7 @@ describe('Cycle analytics utils', () => {
it('will return blank arrays if given no data', () => { it('will return blank arrays if given no data', () => {
[{ data: [], startDate, endDate }, [], {}].forEach(chartData => { [{ data: [], startDate, endDate }, [], {}].forEach(chartData => {
transformed = getTasksByTypeData(chartData); transformed = getTasksByTypeData(chartData);
['seriesNames', 'seriesData', 'range'].forEach(key => { ['seriesNames', 'data', 'groupBy'].forEach(key => {
expect(transformed[key]).toEqual([]); expect(transformed[key]).toEqual([]);
}); });
}); });
...@@ -270,7 +269,7 @@ describe('Cycle analytics utils', () => { ...@@ -270,7 +269,7 @@ describe('Cycle analytics utils', () => {
}); });
it('will return an object with the properties needed for the chart', () => { it('will return an object with the properties needed for the chart', () => {
['seriesNames', 'seriesData', 'range'].forEach(key => { ['seriesNames', 'data', 'groupBy'].forEach(key => {
expect(transformed).toHaveProperty(key); expect(transformed).toHaveProperty(key);
}); });
}); });
...@@ -281,32 +280,32 @@ describe('Cycle analytics utils', () => { ...@@ -281,32 +280,32 @@ describe('Cycle analytics utils', () => {
}); });
}); });
describe('range', () => { describe('groupBy', () => {
it('returns the date range as an array', () => { it('returns the date groupBy as an array', () => {
expect(transformed.range).toEqual(range); expect(transformed.groupBy).toEqual(groupBy);
}); });
it('the start date is the first element', () => { it('the start date is the first element', () => {
expect(transformed.range[0]).toEqual(toYmd(startDate)); expect(transformed.groupBy[0]).toEqual(toYmd(startDate));
}); });
it('the end date is the last element', () => { it('the end date is the last element', () => {
expect(transformed.range[transformed.range.length - 1]).toEqual(toYmd(endDate)); expect(transformed.groupBy[transformed.groupBy.length - 1]).toEqual(toYmd(endDate));
}); });
}); });
describe('data', () => { describe('data', () => {
it('returns an array of data points', () => { it('returns an array of data points', () => {
expect(transformed.seriesData).toEqual(seriesData); expect(transformed.data).toEqual(data);
}); });
it('contains an array of data for each label', () => { it('contains an array of data for each label', () => {
expect(transformed.seriesData.length).toEqual(labels.length); expect(transformed.data.length).toEqual(labels.length);
}); });
it('contains a value for each day in the range', () => { it('contains a value for each day in the groupBy', () => {
transformed.seriesData.forEach(d => { transformed.data.forEach(d => {
expect(d.length).toEqual(transformed.range.length); expect(d.length).toEqual(transformed.groupBy.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