Commit e6242d00 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo Committed by Brandon Labuschagne

Replace legacy stage table with updated stage table

parent b3877915
<script> <script>
import { GlIcon, GlEmptyState, GlLoadingIcon, GlSprintf } from '@gitlab/ui'; import { GlIcon, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue'; import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
import { __ } from '~/locale'; import { __ } from '~/locale';
import banner from './banner.vue';
import stageCodeComponent from './stage_code_component.vue';
import stageComponent from './stage_component.vue';
import stageNavItem from './stage_nav_item.vue';
import stageReviewComponent from './stage_review_component.vue';
import stageStagingComponent from './stage_staging_component.vue';
import stageTestComponent from './stage_test_component.vue';
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
...@@ -18,19 +12,10 @@ export default { ...@@ -18,19 +12,10 @@ export default {
name: 'CycleAnalytics', name: 'CycleAnalytics',
components: { components: {
GlIcon, GlIcon,
GlEmptyState,
GlLoadingIcon, GlLoadingIcon,
GlSprintf, GlSprintf,
banner,
'stage-issue-component': stageComponent,
'stage-plan-component': stageComponent,
'stage-code-component': stageCodeComponent,
'stage-test-component': stageTestComponent,
'stage-review-component': stageReviewComponent,
'stage-staging-component': stageStagingComponent,
'stage-production-component': stageComponent,
'stage-nav-item': stageNavItem,
PathNavigation, PathNavigation,
StageTable,
}, },
props: { props: {
noDataSvgPath: { noDataSvgPath: {
...@@ -75,12 +60,20 @@ export default { ...@@ -75,12 +60,20 @@ export default {
return !this.isLoadingStage && this.selectedStage; return !this.isLoadingStage && this.selectedStage;
}, },
emptyStageTitle() { emptyStageTitle() {
if (this.displayNoAccess) {
return __('You need permission.');
}
return this.selectedStageError return this.selectedStageError
? this.selectedStageError ? this.selectedStageError
: __("We don't have enough data to show this stage."); : __("We don't have enough data to show this stage.");
}, },
emptyStageText() { emptyStageText() {
return !this.selectedStageError ? this.selectedStage.emptyStageText : ''; if (this.displayNoAccess) {
return __('Want to see the data? Please ask an administrator for access.');
}
return !this.selectedStageError && this.selectedStage?.emptyStageText
? this.selectedStage?.emptyStageText
: '';
}, },
}, },
methods: { methods: {
...@@ -160,72 +153,16 @@ export default { ...@@ -160,72 +153,16 @@ export default {
</div> </div>
</div> </div>
</div> </div>
<div class="stage-panel-container" data-testid="vsa-stage-table"> <stage-table
<div class="card stage-panel gl-px-5"> :is-loading="isLoading || isLoadingStage"
<div class="card-header border-bottom-0"> :stage-events="selectedStageEvents"
<nav class="col-headers"> :selected-stage="selectedStage"
<ul class="gl-display-flex gl-justify-content-space-between gl-list-style-none"> :stage-count="null"
<li> :empty-state-title="emptyStageTitle"
<span v-if="selectedStage" class="stage-name font-weight-bold">{{ :empty-state-message="emptyStageText"
selectedStage.legend ? __(selectedStage.legend) : __('Related Issues') :no-data-svg-path="noDataSvgPath"
}}</span> :pagination="null"
<span
class="has-tooltip"
data-placement="top"
:title="
__('The collection of events added to the data gathered for that stage.')
"
aria-hidden="true"
>
<gl-icon name="question-o" class="gl-text-gray-500" />
</span>
</li>
<li>
<span class="stage-name font-weight-bold">{{ __('Time') }}</span>
<span
class="has-tooltip"
data-placement="top"
:title="__('The time taken by each data entry gathered by that stage.')"
aria-hidden="true"
>
<gl-icon name="question-o" class="gl-text-gray-500" />
</span>
</li>
</ul>
</nav>
</div>
<div class="stage-panel-body">
<section class="stage-events gl-overflow-auto gl-w-full">
<gl-loading-icon v-if="isLoadingStage" size="lg" />
<template v-else>
<gl-empty-state
v-if="displayNoAccess"
class="js-empty-state"
:title="__('You need permission.')"
:svg-path="noAccessSvgPath"
:description="__('Want to see the data? Please ask an administrator for access.')"
/>
<template v-else>
<gl-empty-state
v-if="displayNotEnoughData"
class="js-empty-state"
:description="emptyStageText"
:svg-path="noDataSvgPath"
:title="emptyStageTitle"
/>
<component
:is="selectedStage.component"
v-if="displayStageEvents"
:stage="selectedStage"
:items="selectedStageEvents"
data-testid="stage-table-events"
/> />
</template>
</template>
</section>
</div>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
...@@ -17,6 +17,8 @@ import { ...@@ -17,6 +17,8 @@ import {
PAGINATION_SORT_FIELD_DURATION, PAGINATION_SORT_FIELD_DURATION,
PAGINATION_SORT_DIRECTION_ASC, PAGINATION_SORT_DIRECTION_ASC,
PAGINATION_SORT_DIRECTION_DESC, PAGINATION_SORT_DIRECTION_DESC,
STAGE_TITLE_STAGING,
STAGE_TITLE_TEST,
} from '../constants'; } from '../constants';
import TotalTime from './total_time_component.vue'; import TotalTime from './total_time_component.vue';
...@@ -49,7 +51,8 @@ export default { ...@@ -49,7 +51,8 @@ export default {
props: { props: {
selectedStage: { selectedStage: {
type: Object, type: Object,
required: true, required: false,
default: () => ({ custom: false }),
}, },
isLoading: { isLoading: {
type: Boolean, type: Boolean,
...@@ -68,6 +71,11 @@ export default { ...@@ -68,6 +71,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
emptyStateTitle: {
type: String,
required: false,
default: null,
},
emptyStateMessage: { emptyStateMessage: {
type: String, type: String,
required: false, required: false,
...@@ -75,33 +83,41 @@ export default { ...@@ -75,33 +83,41 @@ export default {
}, },
pagination: { pagination: {
type: Object, type: Object,
required: true, required: false,
default: null,
}, },
}, },
data() { data() {
if (this.pagination) {
const { const {
pagination: { sort, direction }, pagination: { sort, direction },
} = this; } = this;
return { return {
sort, sort,
direction,
sortDesc: direction === PAGINATION_SORT_DIRECTION_DESC, sortDesc: direction === PAGINATION_SORT_DIRECTION_DESC,
}; };
}
return { sort: null, direction: null, sortDesc: null };
}, },
computed: { computed: {
isEmptyStage() { isEmptyStage() {
return !this.stageEvents.length; return !this.stageEvents.length;
}, },
emptyStateTitle() { emptyStateTitleText() {
const { emptyStateMessage } = this; return this.emptyStateTitle || NOT_ENOUGH_DATA_ERROR;
return emptyStateMessage || NOT_ENOUGH_DATA_ERROR;
}, },
isDefaultTestStage() { isDefaultTestStage() {
const { selectedStage } = this; const { selectedStage } = this;
return !selectedStage.custom && selectedStage.title?.toLowerCase().trim() === 'test'; return (
!selectedStage.custom && selectedStage.title?.toLowerCase().trim() === STAGE_TITLE_TEST
);
}, },
isDefaultStagingStage() { isDefaultStagingStage() {
const { selectedStage } = this; const { selectedStage } = this;
return !selectedStage.custom && selectedStage.title?.toLowerCase().trim() === 'staging'; return (
!selectedStage.custom && selectedStage.title?.toLowerCase().trim() === STAGE_TITLE_STAGING
);
}, },
isMergeRequestStage() { isMergeRequestStage() {
const [firstEvent] = this.stageEvents; const [firstEvent] = this.stageEvents;
...@@ -139,6 +155,9 @@ export default { ...@@ -139,6 +155,9 @@ export default {
isMrLink(url = '') { isMrLink(url = '') {
return url.includes('/merge_request'); return url.includes('/merge_request');
}, },
itemId({ url, iid }) {
return this.isMrLink(url) ? `!${iid}` : `#${iid}`;
},
itemTitle(item) { itemTitle(item) {
return item.title || item.name; return item.title || item.name;
}, },
...@@ -160,7 +179,12 @@ export default { ...@@ -160,7 +179,12 @@ export default {
<template> <template>
<div data-testid="vsa-stage-table"> <div data-testid="vsa-stage-table">
<gl-loading-icon v-if="isLoading" class="gl-mt-4" size="md" /> <gl-loading-icon v-if="isLoading" class="gl-mt-4" size="md" />
<gl-empty-state v-else-if="isEmptyStage" :title="emptyStateTitle" :svg-path="noDataSvgPath" /> <gl-empty-state
v-else-if="isEmptyStage"
:title="emptyStateTitleText"
:description="emptyStateMessage"
:svg-path="noDataSvgPath"
/>
<gl-table <gl-table
v-else v-else
head-variant="white" head-variant="white"
...@@ -168,18 +192,18 @@ export default { ...@@ -168,18 +192,18 @@ export default {
thead-class="border-bottom" thead-class="border-bottom"
show-empty show-empty
:sort-by.sync="sort" :sort-by.sync="sort"
:sort-direction.sync="pagination.direction" :sort-direction.sync="direction"
:sort-desc.sync="sortDesc" :sort-desc.sync="sortDesc"
:fields="fields" :fields="fields"
:items="stageEvents" :items="stageEvents"
:empty-text="emptyStateMessage" :empty-text="emptyStateMessage"
@sort-changed="onSort" @sort-changed="onSort"
> >
<template #head(end_event)="data"> <template v-if="stageCount" #head(end_event)="data">
<span>{{ data.label }}</span <span>{{ data.label }}</span
><gl-badge class="gl-ml-2" size="sm"> ><gl-badge class="gl-ml-2" size="sm"
<formatted-stage-count :stage-count="stageCount" /> ><formatted-stage-count :stage-count="stageCount"
</gl-badge> /></gl-badge>
</template> </template>
<template #cell(end_event)="{ item }"> <template #cell(end_event)="{ item }">
<div data-testid="vsa-stage-event"> <div data-testid="vsa-stage-event">
...@@ -245,12 +269,7 @@ export default { ...@@ -245,12 +269,7 @@ export default {
<gl-link class="gl-text-black-normal" :href="item.url">{{ itemTitle(item) }}</gl-link> <gl-link class="gl-text-black-normal" :href="item.url">{{ itemTitle(item) }}</gl-link>
</h5> </h5>
<p class="gl-m-0"> <p class="gl-m-0">
<template v-if="isMrLink(item.url)"> <gl-link class="gl-text-black-normal" :href="item.url">{{ itemId(item) }}</gl-link>
<gl-link class="gl-text-black-normal" :href="item.url">!{{ item.iid }}</gl-link>
</template>
<template v-else>
<gl-link class="gl-text-black-normal" :href="item.url">#{{ item.iid }}</gl-link>
</template>
<span class="gl-font-lg">&middot;</span> <span class="gl-font-lg">&middot;</span>
<span data-testid="vsa-stage-event-date"> <span data-testid="vsa-stage-event-date">
{{ s__('OpenedNDaysAgo|Opened') }} {{ s__('OpenedNDaysAgo|Opened') }}
...@@ -273,7 +292,7 @@ export default { ...@@ -273,7 +292,7 @@ export default {
</template> </template>
</gl-table> </gl-table>
<gl-pagination <gl-pagination
v-if="!isLoading && !isEmptyStage" v-if="pagination && !isLoading && !isEmptyStage"
:value="pagination.page" :value="pagination.page"
:prev-page="prevPage" :prev-page="prevPage"
:next-page="nextPage" :next-page="nextPage"
......
<script> <script>
import { n__, s__ } from '~/locale';
export default { export default {
props: { props: {
time: { time: {
...@@ -11,24 +13,48 @@ export default { ...@@ -11,24 +13,48 @@ export default {
hasData() { hasData() {
return Object.keys(this.time).length; return Object.keys(this.time).length;
}, },
calculatedTime() {
const {
time: { days = null, mins = null, hours = null, seconds = null },
} = this;
if (days) {
return {
duration: days,
units: n__('day', 'days', days),
};
}
if (hours) {
return {
duration: hours,
units: n__('Time|hr', 'Time|hrs', hours),
};
}
if (mins && !days) {
return {
duration: mins,
units: n__('Time|min', 'Time|mins', mins),
};
}
if ((seconds && this.hasData === 1) || seconds === 0) {
return {
duration: seconds,
units: s__('Time|s'),
};
}
return { duration: null, units: null };
},
}, },
}; };
</script> </script>
<template> <template>
<span class="total-time"> <span class="total-time">
<template v-if="hasData"> <template v-if="hasData">
<template v-if="time.days"> {{ calculatedTime.duration }} <span>{{ calculatedTime.units }}</span>
{{ time.days }} <span> {{ n__('day', 'days', time.days) }} </span>
</template>
<template v-if="time.hours">
{{ time.hours }} <span> {{ n__('Time|hr', 'Time|hrs', time.hours) }} </span>
</template>
<template v-if="time.mins && !time.days">
{{ time.mins }} <span> {{ n__('Time|min', 'Time|mins', time.mins) }} </span>
</template>
<template v-if="(time.seconds && hasData === 1) || time.seconds === 0">
{{ time.seconds }} <span> {{ s__('Time|s') }} </span>
</template>
</template> </template>
<template v-else> -- </template> <template v-else> -- </template>
</span> </span>
......
import { s__ } from '~/locale';
export const DEFAULT_DAYS_IN_PAST = 30; export const DEFAULT_DAYS_IN_PAST = 30;
export const DEFAULT_DAYS_TO_DISPLAY = 30; export const DEFAULT_DAYS_TO_DISPLAY = 30;
export const OVERVIEW_STAGE_ID = 'overview'; export const OVERVIEW_STAGE_ID = 'overview';
...@@ -7,3 +9,16 @@ export const DEFAULT_VALUE_STREAM = { ...@@ -7,3 +9,16 @@ export const DEFAULT_VALUE_STREAM = {
slug: 'default', slug: 'default',
name: 'default', name: 'default',
}; };
export const NOT_ENOUGH_DATA_ERROR = s__(
"ValueStreamAnalyticsStage|We don't have enough data to show this stage.",
);
export const PAGINATION_TYPE = 'keyset';
export const PAGINATION_SORT_FIELD_END_EVENT = 'end_event';
export const PAGINATION_SORT_FIELD_DURATION = 'duration';
export const PAGINATION_SORT_DIRECTION_DESC = 'desc';
export const PAGINATION_SORT_DIRECTION_ASC = 'asc';
export const STAGE_TITLE_STAGING = 'staging';
export const STAGE_TITLE_TEST = 'test';
...@@ -47,13 +47,7 @@ export default { ...@@ -47,13 +47,7 @@ export default {
state.stages = []; state.stages = [];
}, },
[types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS](state, { stages = [] }) { [types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS](state, { stages = [] }) {
state.stages = stages.map((s) => ({ state.stages = stages.map((s) => convertObjectPropsToCamelCase(s, { deep: true }));
...convertObjectPropsToCamelCase(s, { deep: true }),
// NOTE: we set the component type here to match the current behaviour
// this can be removed when we migrate to the update stage table
// https://gitlab.com/gitlab-org/gitlab/-/issues/326704
component: `stage-${s.id}-component`,
}));
}, },
[types.RECEIVE_VALUE_STREAM_STAGES_ERROR](state) { [types.RECEIVE_VALUE_STREAM_STAGES_ERROR](state) {
state.stages = []; state.stages = [];
......
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
import { GlEmptyState } from '@gitlab/ui'; import { GlEmptyState } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue'; import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue'; import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue';
import { OVERVIEW_STAGE_ID } from '~/cycle_analytics/constants'; import { OVERVIEW_STAGE_ID } from '~/cycle_analytics/constants';
import UrlSync from '~/vue_shared/components/url_sync.vue'; import UrlSync from '~/vue_shared/components/url_sync.vue';
import { toYmd } from '../../shared/utils'; import { toYmd } from '../../shared/utils';
import DurationChart from './duration_chart.vue'; import DurationChart from './duration_chart.vue';
import Metrics from './metrics.vue'; import Metrics from './metrics.vue';
import StageTable from './stage_table.vue';
import TypeOfWorkCharts from './type_of_work_charts.vue'; import TypeOfWorkCharts from './type_of_work_charts.vue';
import ValueStreamSelect from './value_stream_select.vue'; import ValueStreamSelect from './value_stream_select.vue';
......
<script>
import { n__, s__ } from '~/locale';
export default {
props: {
time: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
hasData() {
return Object.keys(this.time).length;
},
calculatedTime() {
const {
time: { days = null, mins = null, hours = null, seconds = null },
} = this;
if (days) {
return {
duration: days,
units: n__('day', 'days', days),
};
} else if (hours) {
return {
duration: hours,
units: n__('Time|hr', 'Time|hrs', hours),
};
} else if (mins && !days) {
return {
duration: mins,
units: n__('Time|min', 'Time|mins', mins),
};
} else if ((seconds && this.hasData === 1) || seconds === 0) {
return {
duration: seconds,
units: s__('Time|s'),
};
}
return { duration: null, units: null };
},
},
};
</script>
<template>
<span class="total-time">
<template v-if="hasData">
{{ calculatedTime.duration }} <span>{{ calculatedTime.units }}</span>
</template>
<template v-else> -- </template>
</span>
</template>
...@@ -33,16 +33,6 @@ export const OVERVIEW_STAGE_CONFIG = { ...@@ -33,16 +33,6 @@ export const OVERVIEW_STAGE_CONFIG = {
icon: 'home', icon: 'home',
}; };
export const NOT_ENOUGH_DATA_ERROR = s__(
"ValueStreamAnalyticsStage|We don't have enough data to show this stage.",
);
export const PAGINATION_TYPE = 'keyset';
export const PAGINATION_SORT_FIELD_END_EVENT = 'end_event';
export const PAGINATION_SORT_FIELD_DURATION = 'duration';
export const PAGINATION_SORT_DIRECTION_DESC = 'desc';
export const PAGINATION_SORT_DIRECTION_ASC = 'asc';
export const METRICS_POPOVER_CONTENT = { export const METRICS_POPOVER_CONTENT = {
'lead-time': { 'lead-time': {
description: s__('ValueStreamAnalytics|Median time from issue created to issue closed.'), description: s__('ValueStreamAnalytics|Median time from issue created to issue closed.'),
......
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { dateFormats } from '~/analytics/shared/constants'; import { dateFormats } from '~/analytics/shared/constants';
import { OVERVIEW_STAGE_ID } from '~/cycle_analytics/constants'; import { OVERVIEW_STAGE_ID, PAGINATION_TYPE } from '~/cycle_analytics/constants';
import { pathNavigationData as basePathNavigationData } from '~/cycle_analytics/store/getters'; import { pathNavigationData as basePathNavigationData } from '~/cycle_analytics/store/getters';
import { filterStagesByHiddenStatus } from '~/cycle_analytics/utils'; import { filterStagesByHiddenStatus } from '~/cycle_analytics/utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import { filterToQueryObject } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; import { filterToQueryObject } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import { DEFAULT_VALUE_STREAM_ID, OVERVIEW_STAGE_CONFIG, PAGINATION_TYPE } from '../constants'; import { DEFAULT_VALUE_STREAM_ID, OVERVIEW_STAGE_CONFIG } from '../constants';
export const hasNoAccessError = (state) => state.errorCode === httpStatus.FORBIDDEN; export const hasNoAccessError = (state) => state.errorCode === httpStatus.FORBIDDEN;
......
import Vue from 'vue'; import Vue from 'vue';
import {
PAGINATION_SORT_FIELD_END_EVENT,
PAGINATION_SORT_DIRECTION_DESC,
} from '~/cycle_analytics/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { PAGINATION_SORT_FIELD_END_EVENT, PAGINATION_SORT_DIRECTION_DESC } from '../constants';
import { transformRawStages, prepareStageErrors, formatMedianValuesWithOverview } from '../utils'; import { transformRawStages, prepareStageErrors, formatMedianValuesWithOverview } from '../utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
......
import { PAGINATION_SORT_FIELD_END_EVENT, PAGINATION_SORT_DIRECTION_DESC } from '../constants'; import {
PAGINATION_SORT_FIELD_END_EVENT,
PAGINATION_SORT_DIRECTION_DESC,
} from '~/cycle_analytics/constants';
export default () => ({ export default () => ({
featureFlags: {}, featureFlags: {},
......
...@@ -6,7 +6,6 @@ import Vuex from 'vuex'; ...@@ -6,7 +6,6 @@ import Vuex from 'vuex';
import Component from 'ee/analytics/cycle_analytics/components/base.vue'; import Component from 'ee/analytics/cycle_analytics/components/base.vue';
import DurationChart from 'ee/analytics/cycle_analytics/components/duration_chart.vue'; import DurationChart from 'ee/analytics/cycle_analytics/components/duration_chart.vue';
import Metrics from 'ee/analytics/cycle_analytics/components/metrics.vue'; import Metrics from 'ee/analytics/cycle_analytics/components/metrics.vue';
import StageTable from 'ee/analytics/cycle_analytics/components/stage_table.vue';
import TypeOfWorkCharts from 'ee/analytics/cycle_analytics/components/type_of_work_charts.vue'; import TypeOfWorkCharts from 'ee/analytics/cycle_analytics/components/type_of_work_charts.vue';
import ValueStreamSelect from 'ee/analytics/cycle_analytics/components/value_stream_select.vue'; import ValueStreamSelect from 'ee/analytics/cycle_analytics/components/value_stream_select.vue';
import createStore from 'ee/analytics/cycle_analytics/store'; import createStore from 'ee/analytics/cycle_analytics/store';
...@@ -19,6 +18,7 @@ import { ...@@ -19,6 +18,7 @@ import {
selectedProjects, selectedProjects,
} from 'jest/cycle_analytics/mock_data'; } from 'jest/cycle_analytics/mock_data';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue'; import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue'; import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue';
import { OVERVIEW_STAGE_ID } from '~/cycle_analytics/constants'; import { OVERVIEW_STAGE_ID } from '~/cycle_analytics/constants';
import createFlash from '~/flash'; import createFlash from '~/flash';
......
import { mount } from '@vue/test-utils';
import TotalTimeComponent from 'ee/analytics/cycle_analytics/components/total_time_component.vue';
describe('TotalTimeComponent', () => {
function createComponent(propsData) {
return mount(TotalTimeComponent, {
propsData,
});
}
let wrapper = null;
afterEach(() => {
wrapper.destroy();
});
describe('with a valid time object', () => {
it.each`
time
${{ seconds: 35 }}
${{ mins: 47, seconds: 3 }}
${{ days: 3, mins: 47, seconds: 3 }}
${{ hours: 23, mins: 10 }}
${{ hours: 7, mins: 20, seconds: 10 }}
`('with $time', ({ time }) => {
wrapper = createComponent({
time,
});
expect(wrapper.html()).toMatchSnapshot();
});
});
describe('with a blank object', () => {
beforeEach(() => {
wrapper = createComponent({
time: {},
});
});
it('to render --', () => {
expect(wrapper.html()).toMatchSnapshot();
});
});
});
...@@ -2,9 +2,6 @@ import { uniq } from 'lodash'; ...@@ -2,9 +2,6 @@ import { uniq } from 'lodash';
import { import {
TASKS_BY_TYPE_SUBJECT_ISSUE, TASKS_BY_TYPE_SUBJECT_ISSUE,
OVERVIEW_STAGE_CONFIG, OVERVIEW_STAGE_CONFIG,
PAGINATION_TYPE,
PAGINATION_SORT_DIRECTION_DESC,
PAGINATION_SORT_FIELD_END_EVENT,
} from 'ee/analytics/cycle_analytics/constants'; } from 'ee/analytics/cycle_analytics/constants';
import * as types from 'ee/analytics/cycle_analytics/store/mutation_types'; import * as types from 'ee/analytics/cycle_analytics/store/mutation_types';
import mutations from 'ee/analytics/cycle_analytics/store/mutations'; import mutations from 'ee/analytics/cycle_analytics/store/mutations';
...@@ -21,6 +18,11 @@ import { ...@@ -21,6 +18,11 @@ import {
createdBefore, createdBefore,
createdAfter, createdAfter,
} from 'jest/cycle_analytics/mock_data'; } from 'jest/cycle_analytics/mock_data';
import {
PAGINATION_TYPE,
PAGINATION_SORT_DIRECTION_DESC,
PAGINATION_SORT_FIELD_END_EVENT,
} from '~/cycle_analytics/constants';
import { transformStagesForPathNavigation } from '~/cycle_analytics/utils'; import { transformStagesForPathNavigation } from '~/cycle_analytics/utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getDatesInRange } from '~/lib/utils/datetime_utility'; import { getDatesInRange } from '~/lib/utils/datetime_utility';
......
import {
PAGINATION_SORT_DIRECTION_DESC,
PAGINATION_SORT_FIELD_END_EVENT,
} from 'ee/analytics/cycle_analytics/constants';
import * as types from 'ee/analytics/cycle_analytics/store/mutation_types'; import * as types from 'ee/analytics/cycle_analytics/store/mutation_types';
import mutations from 'ee/analytics/cycle_analytics/store/mutations'; import mutations from 'ee/analytics/cycle_analytics/store/mutations';
import { createdAfter, createdBefore, selectedProjects } from 'jest/cycle_analytics/mock_data'; import { createdAfter, createdBefore, selectedProjects } from 'jest/cycle_analytics/mock_data';
import {
PAGINATION_SORT_DIRECTION_DESC,
PAGINATION_SORT_FIELD_END_EVENT,
} from '~/cycle_analytics/constants';
import { import {
issueStage, issueStage,
planStage, planStage,
......
...@@ -27061,9 +27061,6 @@ msgstr "" ...@@ -27061,9 +27061,6 @@ msgstr ""
msgid "Rejected (closed)" msgid "Rejected (closed)"
msgstr "" msgstr ""
msgid "Related Issues"
msgstr ""
msgid "Related feature flags" msgid "Related feature flags"
msgstr "" msgstr ""
...@@ -32488,9 +32485,6 @@ msgstr "" ...@@ -32488,9 +32485,6 @@ msgstr ""
msgid "The character highlighter helps you keep the subject line to %{titleLength} characters and wrap the body at %{bodyLength} so they are readable in git." msgid "The character highlighter helps you keep the subject line to %{titleLength} characters and wrap the body at %{bodyLength} so they are readable in git."
msgstr "" msgstr ""
msgid "The collection of events added to the data gathered for that stage."
msgstr ""
msgid "The comment you are editing has been changed by another user. Would you like to keep your changes and overwrite the new description or discard your changes?" msgid "The comment you are editing has been changed by another user. Would you like to keep your changes and overwrite the new description or discard your changes?"
msgstr "" msgstr ""
...@@ -32853,9 +32847,6 @@ msgstr "" ...@@ -32853,9 +32847,6 @@ msgstr ""
msgid "The tag name can't be changed for an existing release." msgid "The tag name can't be changed for an existing release."
msgstr "" msgstr ""
msgid "The time taken by each data entry gathered by that stage."
msgstr ""
msgid "The update action will time out after %{number_of_minutes} minutes. For big repositories, use a clone/push combination." msgid "The update action will time out after %{number_of_minutes} minutes. For big repositories, use a clone/push combination."
msgstr "" msgstr ""
......
...@@ -6,6 +6,7 @@ RSpec.describe 'Value Stream Analytics', :js do ...@@ -6,6 +6,7 @@ RSpec.describe 'Value Stream Analytics', :js do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:guest) { create(:user) } let_it_be(:guest) { create(:user) }
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:stage_table_selector) { '[data-testid="vsa-stage-table"]' }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) } let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
let(:milestone) { create(:milestone, project: project) } let(:milestone) { create(:milestone, project: project) }
...@@ -119,13 +120,13 @@ RSpec.describe 'Value Stream Analytics', :js do ...@@ -119,13 +120,13 @@ RSpec.describe 'Value Stream Analytics', :js do
end end
it 'needs permissions to see restricted stages' do it 'needs permissions to see restricted stages' do
expect(find('.stage-events')).to have_content(issue.title) expect(find(stage_table_selector)).to have_content(issue.title)
click_stage('Code') click_stage('Code')
expect(find('.stage-events')).to have_content('You need permission.') expect(find(stage_table_selector)).to have_content('You need permission.')
click_stage('Review') click_stage('Review')
expect(find('.stage-events')).to have_content('You need permission.') expect(find(stage_table_selector)).to have_content('You need permission.')
end end
end end
...@@ -154,21 +155,21 @@ RSpec.describe 'Value Stream Analytics', :js do ...@@ -154,21 +155,21 @@ RSpec.describe 'Value Stream Analytics', :js do
end end
def expect_issue_to_be_present def expect_issue_to_be_present
expect(find('.stage-events')).to have_content(issue.title) expect(find(stage_table_selector)).to have_content(issue.title)
expect(find('.stage-events')).to have_content(issue.author.name) expect(find(stage_table_selector)).to have_content(issue.author.name)
expect(find('.stage-events')).to have_content("##{issue.iid}") expect(find(stage_table_selector)).to have_content("##{issue.iid}")
end end
def expect_build_to_be_present def expect_build_to_be_present
expect(find('.stage-events')).to have_content(@build.ref) expect(find(stage_table_selector)).to have_content(@build.ref)
expect(find('.stage-events')).to have_content(@build.short_sha) expect(find(stage_table_selector)).to have_content(@build.short_sha)
expect(find('.stage-events')).to have_content("##{@build.id}") expect(find(stage_table_selector)).to have_content("##{@build.id}")
end end
def expect_merge_request_to_be_present def expect_merge_request_to_be_present
expect(find('.stage-events')).to have_content(mr.title) expect(find(stage_table_selector)).to have_content(mr.title)
expect(find('.stage-events')).to have_content(mr.author.name) expect(find(stage_table_selector)).to have_content(mr.author.name)
expect(find('.stage-events')).to have_content("!#{mr.iid}") expect(find(stage_table_selector)).to have_content("!#{mr.iid}")
end end
def click_stage(stage_name) def click_stage(stage_name)
......
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Value stream analytics component isEmptyStage = true renders the empty stage with \`Not enough data\` message 1`] = `"<gl-empty-state-stub title=\\"We don't have enough data to show this stage.\\" svgpath=\\"path/to/no/data\\" description=\\"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.\\" class=\\"js-empty-state\\"></gl-empty-state-stub>"`;
exports[`Value stream analytics component isEmptyStage = true with a selectedStageError renders the empty stage with \`There is too much data to calculate\` message 1`] = `"<gl-empty-state-stub title=\\"There is too much data to calculate\\" svgpath=\\"path/to/no/data\\" description=\\"\\" class=\\"js-empty-state\\"></gl-empty-state-stub>"`;
exports[`Value stream analytics component isLoading = true renders the path navigation component with prop \`loading\` set to true 1`] = `"<path-navigation-stub loading=\\"true\\" stages=\\"\\" selectedstage=\\"[object Object]\\" class=\\"js-path-navigation gl-w-full gl-pb-2\\"></path-navigation-stub>"`; exports[`Value stream analytics component isLoading = true renders the path navigation component with prop \`loading\` set to true 1`] = `"<path-navigation-stub loading=\\"true\\" stages=\\"\\" selectedstage=\\"[object Object]\\" class=\\"js-path-navigation gl-w-full gl-pb-2\\"></path-navigation-stub>"`;
exports[`Value stream analytics component without enough permissions renders the empty stage with \`You need permission\` message 1`] = `"<gl-empty-state-stub title=\\"You need permission.\\" svgpath=\\"path/to/no/access\\" description=\\"Want to see the data? Please ask an administrator for access.\\" class=\\"js-empty-state\\"></gl-empty-state-stub>"`;
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TotalTimeComponent with a blank object to render -- 1`] = `"<span class=\\"total-time\\"> -- </span>"`; exports[`TotalTimeComponent with a blank object should render -- 1`] = `"<span class=\\"total-time\\"> -- </span>"`;
exports[`TotalTimeComponent with a valid time object with {"days": 3, "mins": 47, "seconds": 3} 1`] = ` exports[`TotalTimeComponent with a valid time object with {"days": 3, "mins": 47, "seconds": 3} 1`] = `
"<span class=\\"total-time\\"> "<span class=\\"total-time\\">
......
...@@ -5,6 +5,8 @@ import Vuex from 'vuex'; ...@@ -5,6 +5,8 @@ import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BaseComponent from '~/cycle_analytics/components/base.vue'; import BaseComponent from '~/cycle_analytics/components/base.vue';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue'; import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
import { NOT_ENOUGH_DATA_ERROR } from '~/cycle_analytics/constants';
import initState from '~/cycle_analytics/store/state'; import initState from '~/cycle_analytics/store/state';
import { selectedStage, convertedEvents as selectedStageEvents } from './mock_data'; import { selectedStage, convertedEvents as selectedStageEvents } from './mock_data';
...@@ -38,6 +40,9 @@ function createComponent({ initialState } = {}) { ...@@ -38,6 +40,9 @@ function createComponent({ initialState } = {}) {
noDataSvgPath, noDataSvgPath,
noAccessSvgPath, noAccessSvgPath,
}, },
stubs: {
StageTable,
},
}), }),
); );
} }
...@@ -45,9 +50,9 @@ function createComponent({ initialState } = {}) { ...@@ -45,9 +50,9 @@ function createComponent({ initialState } = {}) {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPathNavigation = () => wrapper.findComponent(PathNavigation); const findPathNavigation = () => wrapper.findComponent(PathNavigation);
const findOverviewMetrics = () => wrapper.findByTestId('vsa-stage-overview-metrics'); const findOverviewMetrics = () => wrapper.findByTestId('vsa-stage-overview-metrics');
const findStageTable = () => wrapper.findByTestId('vsa-stage-table'); const findStageTable = () => wrapper.findComponent(StageTable);
const findEmptyStage = () => wrapper.findComponent(GlEmptyState); const findStageEvents = () => findStageTable().props('stageEvents');
const findStageEvents = () => wrapper.findByTestId('stage-table-events'); const findEmptyStageTitle = () => wrapper.findComponent(GlEmptyState).props('title');
describe('Value stream analytics component', () => { describe('Value stream analytics component', () => {
beforeEach(() => { beforeEach(() => {
...@@ -81,8 +86,7 @@ describe('Value stream analytics component', () => { ...@@ -81,8 +86,7 @@ describe('Value stream analytics component', () => {
}); });
it('renders the stage table events', () => { it('renders the stage table events', () => {
expect(findEmptyStage().exists()).toBe(false); expect(findStageEvents()).toEqual(selectedStageEvents);
expect(findStageEvents().exists()).toBe(true);
}); });
it('does not render the loading icon', () => { it('does not render the loading icon', () => {
...@@ -135,7 +139,7 @@ describe('Value stream analytics component', () => { ...@@ -135,7 +139,7 @@ describe('Value stream analytics component', () => {
}); });
it('renders the empty stage with `Not enough data` message', () => { it('renders the empty stage with `Not enough data` message', () => {
expect(findEmptyStage().html()).toMatchSnapshot(); expect(findEmptyStageTitle()).toBe(NOT_ENOUGH_DATA_ERROR);
}); });
describe('with a selectedStageError', () => { describe('with a selectedStageError', () => {
...@@ -150,7 +154,7 @@ describe('Value stream analytics component', () => { ...@@ -150,7 +154,7 @@ describe('Value stream analytics component', () => {
}); });
it('renders the empty stage with `There is too much data to calculate` message', () => { it('renders the empty stage with `There is too much data to calculate` message', () => {
expect(findEmptyStage().html()).toMatchSnapshot(); expect(findEmptyStageTitle()).toBe('There is too much data to calculate');
}); });
}); });
}); });
...@@ -166,8 +170,8 @@ describe('Value stream analytics component', () => { ...@@ -166,8 +170,8 @@ describe('Value stream analytics component', () => {
}); });
}); });
it('renders the empty stage with `You need permission` message', () => { it('renders the empty stage with `You need permission.` message', () => {
expect(findEmptyStage().html()).toMatchSnapshot(); expect(findEmptyStageTitle()).toBe('You need permission.');
}); });
}); });
...@@ -187,7 +191,7 @@ describe('Value stream analytics component', () => { ...@@ -187,7 +191,7 @@ describe('Value stream analytics component', () => {
}); });
it('does not render the stage table events', () => { it('does not render the stage table events', () => {
expect(findStageEvents().exists()).toBe(false); expect(findStageEvents()).toHaveLength(0);
}); });
it('does not render the loading icon', () => { it('does not render the loading icon', () => {
......
...@@ -18,7 +18,7 @@ export const summary = [ ...@@ -18,7 +18,7 @@ export const summary = [
{ value: null, title: 'Deployment Frequency', unit: 'per day' }, { value: null, title: 'Deployment Frequency', unit: 'per day' },
]; ];
const issueStage = { export const issueStage = {
id: 'issue', id: 'issue',
title: 'Issue', title: 'Issue',
name: 'issue', name: 'issue',
...@@ -27,7 +27,7 @@ const issueStage = { ...@@ -27,7 +27,7 @@ const issueStage = {
value: null, value: null,
}; };
const planStage = { export const planStage = {
id: 'plan', id: 'plan',
title: 'Plan', title: 'Plan',
name: 'plan', name: 'plan',
...@@ -36,7 +36,7 @@ const planStage = { ...@@ -36,7 +36,7 @@ const planStage = {
value: 75600, value: 75600,
}; };
const codeStage = { export const codeStage = {
id: 'code', id: 'code',
title: 'Code', title: 'Code',
name: 'code', name: 'code',
...@@ -45,7 +45,7 @@ const codeStage = { ...@@ -45,7 +45,7 @@ const codeStage = {
value: 172800, value: 172800,
}; };
const testStage = { export const testStage = {
id: 'test', id: 'test',
title: 'Test', title: 'Test',
name: 'test', name: 'test',
...@@ -54,7 +54,7 @@ const testStage = { ...@@ -54,7 +54,7 @@ const testStage = {
value: 17550, value: 17550,
}; };
const reviewStage = { export const reviewStage = {
id: 'review', id: 'review',
title: 'Review', title: 'Review',
name: 'review', name: 'review',
...@@ -63,7 +63,7 @@ const reviewStage = { ...@@ -63,7 +63,7 @@ const reviewStage = {
value: null, value: null,
}; };
const stagingStage = { export const stagingStage = {
id: 'staging', id: 'staging',
title: 'Staging', title: 'Staging',
name: 'staging', name: 'staging',
...@@ -79,7 +79,7 @@ export const selectedStage = { ...@@ -79,7 +79,7 @@ export const selectedStage = {
isUserAllowed: true, isUserAllowed: true,
emptyStageText: emptyStageText:
'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.', 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
component: 'stage-issue-component',
slug: 'issue', slug: 'issue',
}; };
...@@ -290,7 +290,189 @@ export const rawValueStreamStages = [ ...@@ -290,7 +290,189 @@ export const rawValueStreamStages = [
}, },
]; ];
export const valueStreamStages = rawValueStreamStages.map((s) => ({ export const valueStreamStages = rawValueStreamStages.map((s) =>
...convertObjectPropsToCamelCase(s, { deep: true }), convertObjectPropsToCamelCase(s, { deep: true }),
component: `stage-${s.id}-component`, );
}));
// Temporary workaronud until we have relevant backend fixtures endpoints
export const testEvents = [
{
name: 'test',
id: 53,
branch: {
name: 'master',
url: 'http://localhost/group3/project9/-/tree/master',
},
shortSha: 'b83d6e39',
author: {
id: 18,
name: 'John Doe21',
username: 'user12',
state: 'active',
avatarUrl:
'https://www.gravatar.com/avatar/70a85d1042e02066f7451ae831689be0?s=80&d=identicon',
webUrl: 'http://localhost/user12',
showStatus: false,
path: '/user12',
},
date: 'about 1 hour ago',
totalTime: { mins: 2 },
url: 'http://localhost/group3/project9/-/jobs/53',
commitUrl: 'http://localhost/group3/project9/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0',
},
{
name: 'test',
id: 54,
branch: {
name: 'master',
url: 'http://localhost/group3/project9/-/tree/master',
},
shortSha: 'b83d6e39',
author: {
id: 18,
name: 'John Doe21',
username: 'user12',
state: 'active',
avatarUrl:
'https://www.gravatar.com/avatar/70a85d1042e02066f7451ae831689be0?s=80&d=identicon',
webUrl: 'http://localhost/user12',
showStatus: false,
path: '/user12',
},
date: 'about 1 hour ago',
totalTime: { mins: 2 },
url: 'http://localhost/group3/project9/-/jobs/54',
commitUrl: 'http://localhost/group3/project9/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0',
},
];
export const stagingEvents = [
{
name: 'test',
id: 83,
branch: {
name: 'master',
url: 'http://localhost/group3/project9/-/tree/master',
},
shortSha: 'b83d6e39',
author: {
id: 18,
name: 'John Doe21',
username: 'user12',
state: 'active',
avatarUrl:
'https://www.gravatar.com/avatar/70a85d1042e02066f7451ae831689be0?s=80&d=identicon',
webUrl: 'http://localhost/user12',
showStatus: false,
path: '/user12',
},
date: 'about 1 hour ago',
totalTime: { mins: 2 },
url: 'http://localhost/group3/project9/-/jobs/83',
commitUrl: 'http://localhost/group3/project9/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0',
},
{
name: 'test',
id: 84,
branch: {
name: 'master',
url: 'http://localhost/group3/project9/-/tree/master',
},
shortSha: 'b83d6e39',
author: {
id: 18,
name: 'John Doe21',
username: 'user12',
state: 'active',
avatarUrl:
'https://www.gravatar.com/avatar/70a85d1042e02066f7451ae831689be0?s=80&d=identicon',
webUrl: 'http://localhost/user12',
showStatus: false,
path: '/user12',
},
date: 'about 1 hour ago',
totalTime: { mins: 2 },
url: 'http://localhost/group3/project9/-/jobs/84',
commitUrl: 'http://localhost/group3/project9/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0',
},
];
export const reviewEvents = [
{
title: 'My title 98',
author: {
id: 17,
name: 'John Doe20',
username: 'user11',
state: 'active',
avatarUrl:
'https://www.gravatar.com/avatar/fb32cf62136a195ec4f40ec6d1cfffdc?s=80&d=identicon',
webUrl: 'http://localhost/user11',
showStatus: false,
path: '/user11',
},
iid: '3',
totalTime: { days: 15 },
createdAt: '20 days ago',
url: 'http://localhost/group3/project9/-/merge_requests/3',
state: 'opened',
},
{
title: 'My title 99',
author: {
id: 17,
name: 'John Doe20',
username: 'user11',
state: 'active',
avatarUrl:
'https://www.gravatar.com/avatar/fb32cf62136a195ec4f40ec6d1cfffdc?s=80&d=identicon',
webUrl: 'http://localhost/user11',
showStatus: false,
path: '/user11',
},
iid: '4',
totalTime: { days: 9 },
createdAt: '19 days ago',
url: 'http://localhost/group3/project9/-/merge_requests/4',
state: 'opened',
},
];
export const issueEvents = [
{
title: 'My title 24',
author: {
id: 17,
name: 'John Doe20',
username: 'user11',
state: 'active',
avatarUrl:
'https://www.gravatar.com/avatar/fb32cf62136a195ec4f40ec6d1cfffdc?s=80&d=identicon',
webUrl: 'http://localhost/user11',
showStatus: false,
path: '/user11',
},
iid: '3',
totalTime: { days: 2 },
createdAt: '4 days ago',
url: 'http://localhost/group3/project9/-/issues/3',
},
{
title: 'My title 23',
author: {
id: 17,
name: 'John Doe20',
username: 'user11',
state: 'active',
avatarUrl:
'https://www.gravatar.com/avatar/fb32cf62136a195ec4f40ec6d1cfffdc?s=80&d=identicon',
webUrl: 'http://localhost/user11',
showStatus: false,
path: '/user11',
},
iid: '2',
totalTime: { days: 2 },
createdAt: '5 days ago',
url: 'http://localhost/group3/project9/-/issues/2',
},
];
import { GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui'; import { GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
import StageTable from 'ee/analytics/cycle_analytics/components/stage_table.vue';
import { PAGINATION_SORT_FIELD_DURATION } from 'ee/analytics/cycle_analytics/constants';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
import { PAGINATION_SORT_FIELD_DURATION } from '~/cycle_analytics/constants';
import { import {
stagingEvents, stagingEvents,
stagingStage, stagingStage,
...@@ -13,13 +13,13 @@ import { ...@@ -13,13 +13,13 @@ import {
testStage, testStage,
reviewStage, reviewStage,
reviewEvents, reviewEvents,
} from '../mock_data'; } from './mock_data';
let wrapper = null; let wrapper = null;
let trackingSpy = null; let trackingSpy = null;
const noDataSvgPath = 'path/to/no/data'; const noDataSvgPath = 'path/to/no/data';
const emptyStateMessage = 'Too much data'; const emptyStateTitle = 'Too much data';
const notEnoughDataError = "We don't have enough data to show this stage."; const notEnoughDataError = "We don't have enough data to show this stage.";
const [firstIssueEvent] = issueEvents; const [firstIssueEvent] = issueEvents;
const [firstStagingEvent] = stagingEvents; const [firstStagingEvent] = stagingEvents;
...@@ -273,14 +273,14 @@ describe('StageTable', () => { ...@@ -273,14 +273,14 @@ describe('StageTable', () => {
}); });
}); });
describe('emptyStateMessage set', () => { describe('emptyStateTitle set', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ stageEvents: [], emptyStateMessage }); wrapper = createComponent({ stageEvents: [], emptyStateTitle });
}); });
it('will display the custom message', () => { it('will display the custom message', () => {
expect(wrapper.html()).not.toContain(notEnoughDataError); expect(wrapper.html()).not.toContain(notEnoughDataError);
expect(wrapper.html()).toContain(emptyStateMessage); expect(wrapper.html()).toContain(emptyStateTitle);
}); });
}); });
...@@ -300,6 +300,8 @@ describe('StageTable', () => { ...@@ -300,6 +300,8 @@ describe('StageTable', () => {
}); });
it('clicking prev or next will emit an event', async () => { it('clicking prev or next will emit an event', async () => {
expect(wrapper.emitted('handleUpdatePagination')).toBeUndefined();
findPagination().vm.$emit('input', 2); findPagination().vm.$emit('input', 2);
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
...@@ -349,6 +351,7 @@ describe('StageTable', () => { ...@@ -349,6 +351,7 @@ describe('StageTable', () => {
}); });
it('clicking a table column will update the sort field', () => { it('clicking a table column will update the sort field', () => {
expect(wrapper.emitted('handleUpdatePagination')).toBeUndefined();
triggerTableSort(); triggerTableSort();
expect(wrapper.emitted('handleUpdatePagination')[0]).toEqual([ expect(wrapper.emitted('handleUpdatePagination')[0]).toEqual([
...@@ -360,6 +363,7 @@ describe('StageTable', () => { ...@@ -360,6 +363,7 @@ describe('StageTable', () => {
}); });
it('with sortDesc=false will toggle the direction field', async () => { it('with sortDesc=false will toggle the direction field', async () => {
expect(wrapper.emitted('handleUpdatePagination')).toBeUndefined();
triggerTableSort(false); triggerTableSort(false);
expect(wrapper.emitted('handleUpdatePagination')[0]).toEqual([ expect(wrapper.emitted('handleUpdatePagination')[0]).toEqual([
......
import { shallowMount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import TotalTime from '~/cycle_analytics/components/total_time_component.vue'; import TotalTimeComponent from '~/cycle_analytics/components/total_time_component.vue';
describe('Total time component', () => { describe('TotalTimeComponent', () => {
let wrapper; let wrapper = null;
const createComponent = (propsData) => { const createComponent = (propsData) => {
wrapper = shallowMount(TotalTime, { return mount(TotalTimeComponent, {
propsData, propsData,
}); });
}; };
...@@ -14,45 +14,32 @@ describe('Total time component', () => { ...@@ -14,45 +14,32 @@ describe('Total time component', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('With data', () => { describe('with a valid time object', () => {
it('should render information for days and hours', () => { it.each`
createComponent({ time
time: { ${{ seconds: 35 }}
days: 3, ${{ mins: 47, seconds: 3 }}
hours: 4, ${{ days: 3, mins: 47, seconds: 3 }}
}, ${{ hours: 23, mins: 10 }}
${{ hours: 7, mins: 20, seconds: 10 }}
`('with $time', ({ time }) => {
wrapper = createComponent({
time,
}); });
expect(wrapper.text()).toMatchInterpolatedText('3 days 4 hrs'); expect(wrapper.html()).toMatchSnapshot();
}); });
it('should render information for hours and minutes', () => {
createComponent({
time: {
hours: 4,
mins: 35,
},
});
expect(wrapper.text()).toMatchInterpolatedText('4 hrs 35 mins');
}); });
it('should render information for seconds', () => { describe('with a blank object', () => {
createComponent({ beforeEach(() => {
time: { wrapper = createComponent({
seconds: 45, time: {},
},
});
expect(wrapper.text()).toMatchInterpolatedText('45 s');
}); });
}); });
describe('Without data', () => { it('should render --', () => {
it('should render no information', () => { expect(wrapper.html()).toMatchSnapshot();
createComponent();
expect(wrapper.text()).toBe('--');
}); });
}); });
}); });
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