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 {
'errorCode',
'startDate',
'endDate',
// TODO: remove this
'tasksByType',
'medians',
]),
......@@ -101,9 +100,7 @@ export default {
shouldDisplayDurationChart() {
return !this.isLoadingDurationChart && !this.isLoading;
},
shouldDisplayTasksByTypeChart() {
return !this.isLoadingTasksByTypeChart && !this.isLoading && this.tasksByTypeChartData;
},
dateRange: {
get() {
return { startDate: this.startDate, endDate: this.endDate };
......@@ -142,9 +139,6 @@ export default {
hasDurationChart: this.glFeatures.cycleAnalyticsScatterplotEnabled,
hasTasksByTypeChart: this.glFeatures.tasksByTypeChart,
});
console.log('mounted::this.glFeatures', this.glFeatures);
console.log('mounted::this.featureFlags', this.featureFlags);
},
methods: {
...mapActions([
......@@ -332,27 +326,17 @@ export default {
<gl-loading-icon v-else-if="!isLoading" size="md" class="my-4 py-4" />
</template>
<template v-if="featureFlags.hasTasksByTypeChart">
<div v-if="!isLoading">
<gl-loading-icon v-if="isLoadingTasksByTypeChart" size="md" class="my-4 py-4" />
<div v-else class="tasks-by-type-chart">
<div class="tasks-by-type-chart">
<gl-loading-icon
v-if="isLoading || isLoadingTasksByTypeChart"
size="md"
class="my-4 py-4"
/>
<div v-else>
<tasks-by-type-chart
v-if="shouldDisplayTasksByTypeChart"
:data="tasksByTypeChartData.seriesData"
:group-by="tasksByTypeChartData.range"
:series-names="tasksByTypeChartData.seriesNames"
:chart-data="tasksByTypeChartData"
: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>
</template>
......
......@@ -16,22 +16,14 @@ export default {
type: Object,
required: true,
},
data: {
type: Array,
required: true,
},
groupBy: {
type: Array,
required: true,
},
seriesNames: {
type: Array,
chartData: {
type: Object,
required: true,
},
},
computed: {
hasData() {
return this.data && this.data.length;
return this.chartData && this.chartData.data && this.chartData.data.length;
},
selectedFiltersText() {
// TODO: I think we should show labels that have 0 data, currently doesnt appear
......@@ -93,15 +85,22 @@ export default {
<section>
<gl-stacked-column-chart
:option="$options.chartOptions"
:data="data"
:group-by="groupBy"
:data="chartData.data"
:group-by="chartData.groupBy"
x-axis-type="category"
x-axis-title="Date"
y-axis-title="Number of tasks"
:series-names="seriesNames"
:series-names="chartData.seriesNames"
/>
</section>
</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>
</template>
......@@ -131,8 +131,6 @@ export const editCustomStage = ({ commit, dispatch }, selectedStage = {}) => {
export const requestSummaryData = ({ commit }) => commit(types.REQUEST_SUMMARY_DATA);
export const receiveSummaryDataError = ({ commit }, error) => {
console.log('receiveSummaryDataError::error', error);
commit(types.RECEIVE_SUMMARY_DATA_ERROR, error);
createFlash(__('There was an error while fetching cycle analytics summary data.'));
};
......@@ -249,15 +247,14 @@ export const createCustomStage = ({ dispatch, state }, data) => {
};
export const receiveTasksByTypeDataSuccess = ({ commit }, data) => {
console.log('receiveTasksByTypeDataSuccess::data', data);
commit(types.RECEIVE_TASKS_BY_TYPE_DATA_SUCCESS, data);
};
export const receiveTasksByTypeDataError = ({ commit }, error) => {
console.log('receiveTasksByTypeDataError::error', error);
commit(types.RECEIVE_TASKS_BY_TYPE_DATA_ERROR, error);
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 fetchTasksByTypeData = ({ dispatch, state, getters }) => {
......
......@@ -35,5 +35,5 @@ export const tasksByTypeChartData = ({ tasksByType, startDate, endDate }) => {
endDate,
});
}
return null;
return { groupBy: [], data: [], seriesNames: [] };
};
......@@ -78,21 +78,14 @@ export const transformRawStages = (stages = []) =>
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
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),
series: Object.fromEntries(series),
},
{ deep: true },
),
......@@ -217,10 +210,8 @@ export const orderByDate = (a, b) => toUnix(a) - toUnix(b);
// 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 };
};
// overwrites the default values with any value that was returned from the api
const zeroMissingDataPoints = ({ data, defaultData }) => ({ ...defaultData, ...data });
// TODO: docblocks
// Array of values [date, value]
......@@ -233,25 +224,22 @@ export const flattenTaskByTypeSeries = (series = {}) =>
// TODO: docblocks
// GROSS
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) {
return {
range: [],
seriesData: [],
groupBy: [],
data: [],
seriesNames: [],
};
}
const range = getDatesInRange(startDate, endDate, toYmd).sort(orderByDate);
const defaultData = range.reduce(
const groupBy = getDatesInRange(startDate, endDate, toYmd).sort(orderByDate);
const zeroValuesForEachDataPoint = groupBy.reduce(
(acc, date) => ({
...acc,
[date]: 0,
}),
{},
);
// TODO: handle zero's?
// TODO: fixup seeded data so it falls in the correct date range
const transformed = data.reduce(
(acc, curr) => {
......@@ -259,22 +247,22 @@ export const getTasksByTypeData = ({ data = [], startDate = null, endDate = null
label: { title },
series,
} = curr;
// TODO: double check if BE fills in all the dates and adds zeros
acc.seriesNames = [...acc.seriesNames, title];
// TODO: maybe flatmap
// series is already an object at this point
const fullData = zeroMissingDataPoints({ data: series, defaultData });
acc.seriesData = [...acc.seriesData, flattenTaskByTypeSeries(fullData)];
acc.data = [
...acc.data,
// adds 0 values for each data point and overrides with data from the series
flattenTaskByTypeSeries({ ...zeroValuesForEachDataPoint, ...series }),
];
return acc;
},
{
seriesData: [],
data: [],
seriesNames: [],
},
);
return {
...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';
const localVue = createLocalVue();
localVue.use(Vuex);
const defaultStubs = {
'summary-table': true,
'stage-event-list': true,
'stage-nav-item': true,
'tasks-by-type-chart': true,
};
function createComponent({
opts = {},
shallow = true,
......@@ -321,11 +328,7 @@ describe('Cycle Analytics component', () => {
mock = new MockAdapter(axios);
wrapper = createComponent({
opts: {
stubs: {
'summary-table': true,
'stage-event-list': true,
'stage-nav-item': true,
},
stubs: defaultStubs,
},
shallow: false,
withStageSelected: true,
......@@ -343,6 +346,53 @@ describe('Cycle Analytics component', () => {
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 {
getTasksByTypeData,
flattenTaskByTypeSeries,
orderByDate,
arrayToObject, // TODO: dedupe this?
} from 'ee/analytics/cycle_analytics/utils';
import { toYmd } from 'ee/analytics/shared/utils';
import {
......@@ -209,7 +208,7 @@ describe('Cycle analytics utils', () => {
});
describe('flattenTaskByTypeSeries', () => {
const dummySeries = arrayToObject([
const dummySeries = Object.fromEntries([
['2019-01-16', 40],
['2019-01-14', 20],
['2019-01-12', 10],
......@@ -248,8 +247,8 @@ describe('Cycle analytics utils', () => {
describe('getTasksByTypeData', () => {
let transformed = {};
const range = getDatesInRange(startDate, endDate, toYmd);
const seriesData = transformedTasksByTypeData.map(({ series }) => Object.values(series));
const groupBy = getDatesInRange(startDate, endDate, toYmd);
const data = transformedTasksByTypeData.map(({ series }) => Object.values(series));
const labels = transformedTasksByTypeData.map(d => {
const { label } = d;
return label.title;
......@@ -258,7 +257,7 @@ describe('Cycle analytics utils', () => {
it('will return blank arrays if given no data', () => {
[{ data: [], startDate, endDate }, [], {}].forEach(chartData => {
transformed = getTasksByTypeData(chartData);
['seriesNames', 'seriesData', 'range'].forEach(key => {
['seriesNames', 'data', 'groupBy'].forEach(key => {
expect(transformed[key]).toEqual([]);
});
});
......@@ -270,7 +269,7 @@ describe('Cycle analytics utils', () => {
});
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);
});
});
......@@ -281,32 +280,32 @@ describe('Cycle analytics utils', () => {
});
});
describe('range', () => {
it('returns the date range as an array', () => {
expect(transformed.range).toEqual(range);
describe('groupBy', () => {
it('returns the date groupBy as an array', () => {
expect(transformed.groupBy).toEqual(groupBy);
});
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', () => {
expect(transformed.range[transformed.range.length - 1]).toEqual(toYmd(endDate));
expect(transformed.groupBy[transformed.groupBy.length - 1]).toEqual(toYmd(endDate));
});
});
describe('data', () => {
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', () => {
expect(transformed.seriesData.length).toEqual(labels.length);
expect(transformed.data.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);
it('contains a value for each day in the groupBy', () => {
transformed.data.forEach(d => {
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