Skip to content
代码片段 群组 项目
未验证 提交 a645f453 编辑于 作者: Alex Pennells's avatar Alex Pennells 提交者: GitLab
浏览文件

VSA: Split TypeOfWorkCharts into loader/chart components

Refactors the TypeOfWorkCharts component into
TypeOfWorkChartsLoader/TypeOfWorkCharts components. The API logic
has been moved to the loader component to fix the component stories
and for better maintainability.
上级 0a5aea2b
No related branches found
No related tags found
无相关合并请求
......@@ -14,7 +14,7 @@ import UrlSync from '~/vue_shared/components/url_sync.vue';
import PageHeading from '~/vue_shared/components/page_heading.vue';
import { METRICS_REQUESTS } from '../constants';
import DurationChart from './duration_chart.vue';
import TypeOfWorkCharts from './type_of_work_charts.vue';
import TypeOfWorkChartsLoader from './type_of_work_charts_loader.vue';
import ValueStreamAggregationStatus from './value_stream_aggregation_status.vue';
import ValueStreamAggregatingWarning from './value_stream_aggregating_warning.vue';
import ValueStreamEmptyState from './value_stream_empty_state.vue';
......@@ -27,7 +27,7 @@ export default {
PageHeading,
DurationChart,
GlEmptyState,
TypeOfWorkCharts,
TypeOfWorkChartsLoader,
StageTable,
PathNavigation,
ValueStreamAggregationStatus,
......@@ -271,7 +271,7 @@ export default {
<div :class="[isOverviewStageSelected ? 'gl-mt-2' : 'gl-mt-6']">
<duration-overview-chart v-if="isOverviewStageSelected" class="gl-mb-6" />
<duration-chart v-else class="gl-mb-6" />
<type-of-work-charts
<type-of-work-charts-loader
v-if="enableTasksByTypeChart"
v-show="isOverviewStageSelected"
class="gl-mb-6"
......
import { withVuexStore } from 'storybook_addons/vuex_store';
import TypeOfWorkCharts from './type_of_work_charts.vue';
import { defaultGroupLabels } from './stories_constants';
import { tasksByTypeChartData, defaultGroupLabels } from './stories_constants';
export default {
component: TypeOfWorkCharts,
......@@ -8,7 +8,7 @@ export default {
decorators: [withVuexStore],
};
const createStoryWithState = ({ typeOfWork: { getters, state } = {} }) => {
const createStoryWithState = ({ state = {} }) => {
return (args, { argTypes, createVuexStore }) => ({
components: { TypeOfWorkCharts },
props: Object.keys(argTypes),
......@@ -22,31 +22,13 @@ const createStoryWithState = ({ typeOfWork: { getters, state } = {} }) => {
},
getters: {
selectedProjectIds: () => [],
cycleAnalyticsRequestParams: () => ({
project_ids: null,
created_after: '2024-01-01',
created_before: '2024-03-01',
author_username: null,
milestone_title: null,
assignee_username: null,
}),
},
modules: {
typeOfWork: {
namespaced: true,
state,
getters: {
selectedLabelNames: () => [],
...getters,
},
state: {
isLoading: false,
errorMessage: null,
topRankedLabels: [],
...state,
},
actions: {
fetchTopRankedGroupLabels: () => {},
setTasksByTypeFilters: () => {},
},
},
},
......@@ -54,21 +36,13 @@ const createStoryWithState = ({ typeOfWork: { getters, state } = {} }) => {
});
};
const defaultState = {};
export const Default = createStoryWithState(defaultState).bind({});
export const Default = createStoryWithState({}).bind({});
Default.args = { chartData: tasksByTypeChartData };
const noDataState = { typeOfWork: { state: { data: [] } } };
export const NoData = createStoryWithState(noDataState).bind({});
export const NoData = createStoryWithState({}).bind({});
NoData.args = { chartData: { data: [] } };
const isLoadingState = {
typeOfWork: { state: { isLoading: true } },
};
export const IsLoading = createStoryWithState(isLoadingState).bind({});
const errorState = {
typeOfWork: {
...noDataState.typeOfWork,
state: { errorMessage: 'Failed to load chart' },
},
};
export const ErrorMessage = createStoryWithState(errorState).bind({});
export const ErrorMessage = createStoryWithState({
state: { errorMessage: 'Failed to load chart' },
}).bind({});
ErrorMessage.args = { chartData: { data: [] } };
<script>
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
import { mapGetters, mapState } from 'vuex';
import { GlAlert, GlIcon, GlTooltip } from '@gitlab/ui';
import { __ } from '~/locale';
import SafeHtml from '~/vue_shared/directives/safe_html';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import { getTypeOfWorkTasksByType } from 'ee/api/analytics_api';
import {
generateFilterTextDescription,
getTasksByTypeData,
checkForDataError,
alertErrorIfStatusNotOk,
transformRawTasksByTypeData,
} from '../utils';
import { generateFilterTextDescription } from '../utils';
import { formattedDate } from '../../shared/utils';
import { TASKS_BY_TYPE_SUBJECT_ISSUE, TASKS_BY_TYPE_SUBJECT_FILTER_OPTIONS } from '../constants';
import TasksByTypeChart from './tasks_by_type/chart.vue';
......@@ -22,7 +13,6 @@ import NoDataAvailableState from './no_data_available_state.vue';
export default {
name: 'TypeOfWorkCharts',
components: {
ChartSkeletonLoader,
GlAlert,
GlIcon,
GlTooltip,
......@@ -33,29 +23,20 @@ export default {
directives: {
SafeHtml,
},
data() {
return {
tasksByType: [],
isLoadingTasksByType: false,
};
props: {
chartData: {
type: Object,
required: true,
},
},
computed: {
...mapState(['namespace', 'createdAfter', 'createdBefore']),
...mapState('typeOfWork', ['subject', 'errorMessage', 'isLoading']),
...mapGetters(['selectedProjectIds', 'cycleAnalyticsRequestParams']),
...mapState('typeOfWork', ['subject', 'errorMessage']),
...mapGetters(['selectedProjectIds']),
...mapGetters('typeOfWork', ['selectedLabelNames']),
chartData() {
const { tasksByType, createdAfter, createdBefore } = this;
return tasksByType.length
? getTasksByTypeData({ data: tasksByType, createdAfter, createdBefore })
: { groupBy: [], data: [] };
},
hasData() {
return Boolean(this.chartData?.data.length);
},
hasError() {
return this.errorMessage && this.errorMessage !== '';
},
tooltipText() {
return generateFilterTextDescription({
groupName: this.namespace.name,
......@@ -76,96 +57,31 @@ export default {
TASKS_BY_TYPE_SUBJECT_FILTER_OPTIONS[TASKS_BY_TYPE_SUBJECT_ISSUE]
);
},
tasksByTypeParams() {
const {
subject,
selectedLabelNames,
cycleAnalyticsRequestParams: {
project_ids,
created_after,
created_before,
author_username,
milestone_title,
assignee_username,
},
} = this;
return {
project_ids,
created_after,
created_before,
author_username,
milestone_title,
assignee_username,
subject,
label_names: selectedLabelNames,
};
},
},
async created() {
await this.fetchTopRankedGroupLabels();
if (!this.hasError) {
this.fetchTasksByType();
}
},
methods: {
...mapActions('typeOfWork', ['fetchTopRankedGroupLabels', 'setTasksByTypeFilters']),
onUpdateFilter(e) {
this.setTasksByTypeFilters(e);
this.fetchTasksByType();
},
fetchTasksByType() {
// dont request if we have no labels selected
if (!this.selectedLabelNames.length) {
this.tasksByType = [];
return;
}
this.isLoadingTasksByType = true;
getTypeOfWorkTasksByType(this.namespace.fullPath, this.tasksByTypeParams)
.then(checkForDataError)
.then(({ data }) => {
this.tasksByType = transformRawTasksByTypeData(data);
})
.catch((error) => {
alertErrorIfStatusNotOk({
error,
message: __('There was an error fetching data for the tasks by type chart'),
});
})
.finally(() => {
this.isLoadingTasksByType = false;
});
},
},
};
</script>
<template>
<div class="js-tasks-by-type-chart">
<chart-skeleton-loader v-if="isLoading || isLoadingTasksByType" class="gl-my-4 gl-py-4" />
<div v-else>
<div class="gl-flex gl-justify-between">
<h4 class="gl-mt-0">
{{ s__('ValueStreamAnalytics|Tasks by type') }}&nbsp;
<span ref="tooltipTrigger" data-testid="vsa-task-by-type-description">
<gl-icon name="information-o" />
</span>
<gl-tooltip :target="() => $refs.tooltipTrigger" boundary="viewport" placement="top">
<span v-safe-html="tooltipText"></span>
</gl-tooltip>
</h4>
<tasks-by-type-filters
:selected-label-names="selectedLabelNames"
:subject-filter="selectedSubjectFilter"
@update-filter="onUpdateFilter"
/>
</div>
<tasks-by-type-chart v-if="hasData" :data="chartData.data" :group-by="chartData.groupBy" />
<gl-alert v-else-if="errorMessage" variant="info" :dismissible="false" class="gl-mt-3">
{{ errorMessage }}
</gl-alert>
<no-data-available-state v-else />
<div>
<div class="gl-flex gl-justify-between">
<h4 class="gl-mt-0">
{{ s__('ValueStreamAnalytics|Tasks by type') }}&nbsp;
<span ref="tooltipTrigger" data-testid="vsa-task-by-type-description">
<gl-icon name="information-o" />
</span>
<gl-tooltip :target="() => $refs.tooltipTrigger" boundary="viewport" placement="top">
<span v-safe-html="tooltipText"></span>
</gl-tooltip>
</h4>
<tasks-by-type-filters
:selected-label-names="selectedLabelNames"
:subject-filter="selectedSubjectFilter"
@update-filter="$emit('update-filter', $event)"
/>
</div>
<tasks-by-type-chart v-if="hasData" :data="chartData.data" :group-by="chartData.groupBy" />
<gl-alert v-else-if="errorMessage" variant="info" :dismissible="false" class="gl-mt-3">
{{ errorMessage }}
</gl-alert>
<no-data-available-state v-else />
</div>
</template>
<script>
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
import { __ } from '~/locale';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import { getTypeOfWorkTasksByType } from 'ee/api/analytics_api';
import {
getTasksByTypeData,
checkForDataError,
alertErrorIfStatusNotOk,
transformRawTasksByTypeData,
} from '../utils';
import TypeOfWorkCharts from './type_of_work_charts.vue';
export default {
name: 'TypeOfWorkChartsLoader',
components: {
ChartSkeletonLoader,
TypeOfWorkCharts,
},
data() {
return {
tasksByType: [],
isLoadingTasksByType: false,
};
},
computed: {
...mapState(['namespace', 'createdAfter', 'createdBefore']),
...mapState('typeOfWork', ['subject', 'errorMessage', 'isLoading']),
...mapGetters(['cycleAnalyticsRequestParams']),
...mapGetters('typeOfWork', ['selectedLabelNames']),
chartData() {
const { tasksByType, createdAfter, createdBefore } = this;
return tasksByType.length
? getTasksByTypeData({ data: tasksByType, createdAfter, createdBefore })
: { groupBy: [], data: [] };
},
hasError() {
return this.errorMessage && this.errorMessage !== '';
},
tasksByTypeParams() {
const {
subject,
selectedLabelNames,
cycleAnalyticsRequestParams: {
project_ids,
created_after,
created_before,
author_username,
milestone_title,
assignee_username,
},
} = this;
return {
project_ids,
created_after,
created_before,
author_username,
milestone_title,
assignee_username,
subject,
label_names: selectedLabelNames,
};
},
},
async created() {
await this.fetchTopRankedGroupLabels();
if (!this.hasError) {
this.fetchTasksByType();
}
},
methods: {
...mapActions('typeOfWork', ['fetchTopRankedGroupLabels', 'setTasksByTypeFilters']),
onUpdateFilter(e) {
this.setTasksByTypeFilters(e);
this.fetchTasksByType();
},
fetchTasksByType() {
// dont request if we have no labels selected
if (!this.selectedLabelNames.length) {
this.tasksByType = [];
return;
}
this.isLoadingTasksByType = true;
getTypeOfWorkTasksByType(this.namespace.fullPath, this.tasksByTypeParams)
.then(checkForDataError)
.then(({ data }) => {
this.tasksByType = transformRawTasksByTypeData(data);
})
.catch((error) => {
alertErrorIfStatusNotOk({
error,
message: __('There was an error fetching data for the tasks by type chart'),
});
})
.finally(() => {
this.isLoadingTasksByType = false;
});
},
},
};
</script>
<template>
<div class="js-tasks-by-type-chart">
<chart-skeleton-loader v-if="isLoading || isLoadingTasksByType" class="gl-my-4 gl-py-4" />
<type-of-work-charts v-else :chart-data="chartData" @update-filter="onUpdateFilter" />
</div>
</template>
......@@ -8,7 +8,7 @@ import Vuex from 'vuex';
import Component from 'ee/analytics/cycle_analytics/components/base.vue';
import DurationChart from 'ee/analytics/cycle_analytics/components/duration_chart.vue';
import DurationOverviewChart from 'ee/analytics/cycle_analytics/components/duration_overview_chart.vue';
import TypeOfWorkCharts from 'ee/analytics/cycle_analytics/components/type_of_work_charts.vue';
import TypeOfWorkChartsLoader from 'ee/analytics/cycle_analytics/components/type_of_work_charts_loader.vue';
import ValueStreamSelect from 'ee/analytics/cycle_analytics/components/value_stream_select.vue';
import ValueStreamAggregationStatus from 'ee/analytics/cycle_analytics/components/value_stream_aggregation_status.vue';
import ValueStreamEmptyState from 'ee/analytics/cycle_analytics/components/value_stream_empty_state.vue';
......@@ -188,7 +188,7 @@ describe('EE Value Stream Analytics component', () => {
const findFilterBar = () => wrapper.findComponent(ValueStreamFilters);
const findDurationChart = () => wrapper.findComponent(DurationChart);
const findDurationOverviewChart = () => wrapper.findComponent(DurationOverviewChart);
const findTypeOfWorkCharts = () => wrapper.findComponent(TypeOfWorkCharts);
const findTypeOfWorkCharts = () => wrapper.findComponent(TypeOfWorkChartsLoader);
const findValueStreamSelect = () => wrapper.findComponent(ValueStreamSelect);
const findUrlSync = () => wrapper.findComponent(UrlSync);
......
import { shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import waitForPromises from 'helpers/wait_for_promises';
import TypeOfWorkChartsLoader from 'ee/analytics/cycle_analytics/components/type_of_work_charts_loader.vue';
import TypeOfWorkCharts from 'ee/analytics/cycle_analytics/components/type_of_work_charts.vue';
import typeOfWorkModule from 'ee/analytics/cycle_analytics/store/modules/type_of_work';
import {
TASKS_BY_TYPE_SUBJECT_MERGE_REQUEST,
TASKS_BY_TYPE_FILTERS,
TASKS_BY_TYPE_SUBJECT_ISSUE,
} from 'ee/analytics/cycle_analytics/constants';
import { createAlert } from '~/alert';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import { rawTasksByTypeData, groupLabelNames, endpoints } from '../mock_data';
Vue.use(Vuex);
jest.mock('~/alert');
describe('TypeOfWorkChartsLoader', () => {
let wrapper;
let mock;
const fetchTopRankedGroupLabels = jest.fn();
const setTasksByTypeFilters = jest.fn();
const cycleAnalyticsRequestParams = {
project_ids: null,
created_after: '2019-12-11',
created_before: '2020-01-10',
author_username: null,
milestone_title: null,
assignee_username: null,
};
const createStore = (state) =>
new Vuex.Store({
state: {
namespace: {
fullPath: 'fake/group/path',
},
createdAfter: new Date('2019-12-11'),
createdBefore: new Date('2020-01-10'),
},
getters: {
cycleAnalyticsRequestParams: () => cycleAnalyticsRequestParams,
},
modules: {
typeOfWork: {
...typeOfWorkModule,
state: {
subject: TASKS_BY_TYPE_SUBJECT_ISSUE,
...typeOfWorkModule.state,
...state,
},
getters: {
...typeOfWorkModule.getters,
selectedLabelNames: () => groupLabelNames,
},
actions: {
...typeOfWorkModule.actions,
fetchTopRankedGroupLabels,
setTasksByTypeFilters,
},
},
},
});
const createWrapper = ({ state = {} } = {}) => {
wrapper = shallowMount(TypeOfWorkChartsLoader, {
store: createStore(state),
});
return waitForPromises();
};
const findLoader = () => wrapper.findComponent(ChartSkeletonLoader);
const findTypeOfWorkCharts = () => wrapper.findComponent(TypeOfWorkCharts);
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('when loading', () => {
beforeEach(() => {
createWrapper({ state: { isLoading: true } });
});
it('renders skeleton loader', () => {
expect(findLoader().exists()).toBe(true);
});
});
describe('with data', () => {
beforeEach(() => {
mock.onGet(endpoints.tasksByTypeData).replyOnce(HTTP_STATUS_OK, rawTasksByTypeData);
return createWrapper();
});
it('calls the `fetchTopRankedGroupLabels` action', () => {
expect(fetchTopRankedGroupLabels).toHaveBeenCalled();
});
it('fetches tasks by type', () => {
expect(mock.history.get.length).toBe(1);
expect(mock.history.get[0]).toEqual(
expect.objectContaining({
url: '/fake/group/path/-/analytics/type_of_work/tasks_by_type',
params: {
...cycleAnalyticsRequestParams,
subject: TASKS_BY_TYPE_SUBJECT_ISSUE,
label_names: groupLabelNames,
},
}),
);
});
it('renders the type of work charts', () => {
expect(findTypeOfWorkCharts().exists()).toBe(true);
});
it('does not render the loading icon', () => {
expect(findLoader().exists()).toBe(false);
});
describe('when update filter is emitted', () => {
const payload = {
filter: TASKS_BY_TYPE_FILTERS.SUBJECT,
value: TASKS_BY_TYPE_SUBJECT_MERGE_REQUEST,
};
beforeEach(() => {
findTypeOfWorkCharts(wrapper).vm.$emit('update-filter', payload);
});
it('calls the setTasksByTypeFilters method', () => {
expect(setTasksByTypeFilters).toHaveBeenCalledWith(expect.any(Object), payload);
});
it('refetches the tasks by type', () => {
expect(mock.history.get.length).toBe(2);
expect(mock.history.get[1]).toEqual(
expect.objectContaining({
url: '/fake/group/path/-/analytics/type_of_work/tasks_by_type',
params: {
...cycleAnalyticsRequestParams,
subject: TASKS_BY_TYPE_SUBJECT_ISSUE,
label_names: groupLabelNames,
},
}),
);
});
});
});
describe('when tasks by type returns 200 with a data error', () => {
beforeEach(() => {
mock.onGet(endpoints.tasksByTypeData).replyOnce(HTTP_STATUS_OK, { error: 'Too much data' });
return createWrapper();
});
it('does not show an alert', () => {
expect(createAlert).not.toHaveBeenCalled();
});
});
describe('when tasks by type throws an error', () => {
beforeEach(() => {
mock.onGet(endpoints.tasksByTypeData).replyOnce(HTTP_STATUS_NOT_FOUND, { error: 'error' });
return createWrapper();
});
it('shows an error alert', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'There was an error fetching data for the tasks by type chart',
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import waitForPromises from 'helpers/wait_for_promises';
import TasksByTypeChart from 'ee/analytics/cycle_analytics/components/tasks_by_type/chart.vue';
import TasksByTypeFilters from 'ee/analytics/cycle_analytics/components/tasks_by_type/filters.vue';
......@@ -16,44 +13,24 @@ import {
TASKS_BY_TYPE_FILTERS,
TASKS_BY_TYPE_SUBJECT_ISSUE,
} from 'ee/analytics/cycle_analytics/constants';
import { createAlert } from '~/alert';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import { rawTasksByTypeData, groupLabelNames, endpoints } from '../mock_data';
import { tasksByTypeData, groupLabelNames } from '../mock_data';
Vue.use(Vuex);
jest.mock('~/alert');
describe('TypeOfWorkCharts', () => {
let wrapper;
let mock;
const fetchTopRankedGroupLabels = jest.fn();
const setTasksByTypeFilters = jest.fn();
const cycleAnalyticsRequestParams = {
project_ids: null,
created_after: '2019-12-11',
created_before: '2020-01-10',
author_username: null,
milestone_title: null,
assignee_username: null,
};
const createStore = (state, rootGetters) =>
new Vuex.Store({
state: {
namespace: {
fullPath: 'fake/group/path',
name: 'Gitlab Org',
type: 'Group',
},
createdAfter: new Date('2019-12-11'),
createdBefore: new Date('2020-01-10'),
},
getters: {
namespacePath: () => 'fake/group/path',
selectedProjectIds: () => [],
cycleAnalyticsRequestParams: () => cycleAnalyticsRequestParams,
...rootGetters,
},
modules: {
......@@ -68,22 +45,20 @@ describe('TypeOfWorkCharts', () => {
...typeOfWorkModule.getters,
selectedLabelNames: () => groupLabelNames,
},
actions: {
...typeOfWorkModule.actions,
fetchTopRankedGroupLabels,
setTasksByTypeFilters,
},
},
},
});
const createWrapper = ({ state = {}, rootGetters = {}, stubs = {} } = {}) => {
const createWrapper = ({ state = {}, rootGetters = {}, props = {} } = {}) => {
wrapper = shallowMount(TypeOfWorkCharts, {
store: createStore(state, rootGetters),
stubs: {
TasksByTypeChart: true,
TasksByTypeFilters: true,
...stubs,
},
propsData: {
chartData: tasksByTypeData,
...props,
},
});
......@@ -92,53 +67,18 @@ describe('TypeOfWorkCharts', () => {
const findSubjectFilters = () => wrapper.findComponent(TasksByTypeFilters);
const findTasksByTypeChart = () => wrapper.findComponent(TasksByTypeChart);
const findLoader = () => wrapper.findComponent(ChartSkeletonLoader);
const findNoDataAvailableState = () => wrapper.findComponent(NoDataAvailableState);
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('when loading', () => {
beforeEach(() => {
createWrapper({ state: { isLoading: true } });
});
it('renders skeleton loader', () => {
expect(findLoader().exists()).toBe(true);
});
});
describe('with data', () => {
beforeEach(() => {
mock.onGet(endpoints.tasksByTypeData).replyOnce(HTTP_STATUS_OK, rawTasksByTypeData);
return createWrapper();
});
it('calls the `fetchTopRankedGroupLabels` action', () => {
expect(fetchTopRankedGroupLabels).toHaveBeenCalled();
});
it('fetches tasks by type', () => {
expect(mock.history.get.length).toBe(1);
expect(mock.history.get[0]).toEqual(
expect.objectContaining({
url: '/fake/group/path/-/analytics/type_of_work/tasks_by_type',
params: {
...cycleAnalyticsRequestParams,
subject: TASKS_BY_TYPE_SUBJECT_ISSUE,
label_names: groupLabelNames,
},
}),
);
createWrapper();
});
it('renders the task by type chart', () => {
expect(findTasksByTypeChart().exists()).toBe(true);
expect(findTasksByTypeChart().props()).toEqual({
data: tasksByTypeData.data,
groupBy: tasksByTypeData.groupBy,
});
});
it('renders a description of the current filters', () => {
......@@ -147,81 +87,39 @@ describe('TypeOfWorkCharts', () => {
);
});
it('does not render the loading icon', () => {
expect(findLoader(wrapper).exists()).toBe(false);
it('renders the subject filters', () => {
expect(findSubjectFilters().props()).toEqual(
expect.objectContaining({
selectedLabelNames: groupLabelNames,
subjectFilter: TASKS_BY_TYPE_SUBJECT_ISSUE,
}),
);
});
describe('when a filter is selected', () => {
it('emits the update-filter when a filter is selected', () => {
const payload = {
filter: TASKS_BY_TYPE_FILTERS.SUBJECT,
value: TASKS_BY_TYPE_SUBJECT_MERGE_REQUEST,
};
beforeEach(() => {
findSubjectFilters(wrapper).vm.$emit('update-filter', payload);
});
it('calls the setTasksByTypeFilters method', () => {
expect(setTasksByTypeFilters).toHaveBeenCalledWith(expect.any(Object), payload);
});
findSubjectFilters().vm.$emit('update-filter', payload);
it('refetches the tasks by type', () => {
expect(mock.history.get.length).toBe(2);
expect(mock.history.get[1]).toEqual(
expect.objectContaining({
url: '/fake/group/path/-/analytics/type_of_work/tasks_by_type',
params: {
...cycleAnalyticsRequestParams,
subject: TASKS_BY_TYPE_SUBJECT_ISSUE,
label_names: groupLabelNames,
},
}),
);
});
});
});
describe('when tasks by type returns 200 with a data error', () => {
beforeEach(() => {
mock.onGet(endpoints.tasksByTypeData).replyOnce(HTTP_STATUS_OK, { error: 'Too much data' });
return createWrapper();
});
it('does not show an alert', () => {
expect(createAlert).not.toHaveBeenCalled();
});
});
describe('when tasks by type throws an error', () => {
beforeEach(() => {
mock.onGet(endpoints.tasksByTypeData).replyOnce(HTTP_STATUS_NOT_FOUND, { error: 'error' });
return createWrapper();
});
it('shows an error alert', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'There was an error fetching data for the tasks by type chart',
});
expect(wrapper.emitted('update-filter')[0][0]).toEqual(payload);
});
});
describe('with selected projects', () => {
const createWithProjects = (projectIds) =>
createWrapper({
rootGetters: {
selectedProjectIds: () => projectIds,
},
});
it('renders multiple selected project counts', () => {
createWrapper({ rootGetters: { selectedProjectIds: () => [1, 2] } });
it('renders multiple selected project counts', async () => {
await createWithProjects([1, 2]);
expect(wrapper.text()).toContain(
"Shows issues and 3 labels for group 'Gitlab Org' and 2 projects from Dec 11, 2019 to Jan 10, 2020",
);
});
it('renders one selected project count', async () => {
await createWithProjects([1]);
it('renders one selected project count', () => {
createWrapper({ rootGetters: { selectedProjectIds: () => [1] } });
expect(wrapper.text()).toContain(
"Shows issues and 3 labels for group 'Gitlab Org' and 1 project from Dec 11, 2019 to Jan 10, 2020",
);
......@@ -230,15 +128,15 @@ describe('TypeOfWorkCharts', () => {
describe('with no data', () => {
beforeEach(() => {
return createWrapper({ state: { data: [] } });
createWrapper({ props: { chartData: { data: [] } } });
});
it('does not renders the task by type chart', () => {
expect(findTasksByTypeChart(wrapper).exists()).toBe(false);
expect(findTasksByTypeChart().exists()).toBe(false);
});
it('renders the no data available message', () => {
expect(findNoDataAvailableState(wrapper).exists()).toBe(true);
expect(findNoDataAvailableState().exists()).toBe(true);
});
});
});
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册