Commit 78169e93 authored by Luke Duncalfe's avatar Luke Duncalfe

Merge branch '323982-remove-enable-value-stream-analytics-horizontal-navigation-ff' into 'master'

Remove `value_stream_analytics_path_navigation` feature flag [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!60449
parents cf2ba49f ec5f9c24
...@@ -189,9 +189,7 @@ GitLab allows users to create multiple value streams, hide default stages and cr ...@@ -189,9 +189,7 @@ GitLab allows users to create multiple value streams, hide default stages and cr
### Stage path ### Stage path
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/210315) in GitLab 13.0. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/210315) in GitLab 13.0.
> - It's [deployed behind a feature flag](../../feature_flags.md), enabled by default. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/323982) in GitLab 13.12.
> - It's enabled on GitLab.com.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](../../../administration/feature_flags.md). **(FREE SELF)**
![Value stream path navigation](img/vsa_path_nav_v13_11.png "Value stream path navigation") ![Value stream path navigation](img/vsa_path_nav_v13_11.png "Value stream path navigation")
...@@ -213,14 +211,6 @@ Hovering over a stage item displays a popover with the following information: ...@@ -213,14 +211,6 @@ Hovering over a stage item displays a popover with the following information:
- Start event description for the given stage - Start event description for the given stage
- End event description - End event description
Horizontal path navigation is enabled by default. If you have a self-managed instance, an
administrator can [open a Rails console](../../../administration/troubleshooting/navigating_gitlab_via_rails_console.md)
and disable it with the following command:
```ruby
Feature.disable(:value_stream_analytics_path_navigation)
```
### Adding a stage ### Adding a stage
In the following example we're creating a new stage that measures and tracks issues from creation In the following example we're creating a new stage that measures and tracks issues from creation
......
...@@ -7,13 +7,10 @@ import ProjectsDropdownFilter from '../../shared/components/projects_dropdown_fi ...@@ -7,13 +7,10 @@ import ProjectsDropdownFilter from '../../shared/components/projects_dropdown_fi
import { DATE_RANGE_LIMIT } from '../../shared/constants'; import { DATE_RANGE_LIMIT } from '../../shared/constants';
import { toYmd } from '../../shared/utils'; import { toYmd } from '../../shared/utils';
import { PROJECTS_PER_PAGE, OVERVIEW_STAGE_ID } from '../constants'; import { PROJECTS_PER_PAGE, OVERVIEW_STAGE_ID } from '../constants';
import CustomStageForm from './custom_stage_form.vue';
import DurationChart from './duration_chart.vue'; import DurationChart from './duration_chart.vue';
import FilterBar from './filter_bar.vue'; import FilterBar from './filter_bar.vue';
import Metrics from './metrics.vue'; import Metrics from './metrics.vue';
import PathNavigation from './path_navigation.vue'; import PathNavigation from './path_navigation.vue';
import StageTable from './stage_table.vue';
import StageTableNav from './stage_table_nav.vue';
import StageTableNew from './stage_table_new.vue'; import StageTableNew from './stage_table_new.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';
...@@ -25,10 +22,7 @@ export default { ...@@ -25,10 +22,7 @@ export default {
DurationChart, DurationChart,
GlEmptyState, GlEmptyState,
ProjectsDropdownFilter, ProjectsDropdownFilter,
StageTable,
TypeOfWorkCharts, TypeOfWorkCharts,
CustomStageForm,
StageTableNav,
StageTableNew, StageTableNew,
PathNavigation, PathNavigation,
FilterBar, FilterBar,
...@@ -89,31 +83,14 @@ export default { ...@@ -89,31 +83,14 @@ export default {
return !this.currentGroup && !this.isLoading; return !this.currentGroup && !this.isLoading;
}, },
shouldDisplayFilters() { shouldDisplayFilters() {
return !this.errorCode; return !this.errorCode && !this.hasNoAccessError;
}, },
shouldDisplayDurationChart() { shouldDisplayDurationChart() {
return ( return this.featureFlags.hasDurationChart;
!this.featureFlags.hasPathNavigation ||
(this.featureFlags.hasDurationChart &&
this.isOverviewStageSelected &&
!this.hasNoAccessError)
);
},
shouldDisplayTypeOfWorkCharts() {
return (
!this.featureFlags.hasPathNavigation ||
(this.isOverviewStageSelected && !this.hasNoAccessError)
);
}, },
selectedStageReady() { selectedStageReady() {
return !this.hasNoAccessError && this.selectedStage; return !this.hasNoAccessError && this.selectedStage;
}, },
shouldDisplayPathNavigation() {
return this.featureFlags.hasPathNavigation && this.selectedStageReady;
},
shouldDisplayVerticalNavigation() {
return !this.featureFlags.hasPathNavigation && this.selectedStageReady;
},
shouldDisplayCreateMultipleValueStreams() { shouldDisplayCreateMultipleValueStreams() {
return Boolean(!this.shouldRenderEmptyState && !this.isLoadingValueStreams); return Boolean(!this.shouldRenderEmptyState && !this.isLoadingValueStreams);
}, },
...@@ -122,12 +99,6 @@ export default { ...@@ -122,12 +99,6 @@ export default {
}, },
query() { query() {
const selectedProjectIds = this.selectedProjectIds?.length ? this.selectedProjectIds : null; const selectedProjectIds = this.selectedProjectIds?.length ? this.selectedProjectIds : null;
const stageParams = this.featureFlags.hasPathNavigation
? {
sort: (!this.isOverviewStageSelected && this.pagination?.sort) || null,
direction: (!this.isOverviewStageSelected && this.pagination?.direction) || null,
}
: {};
return { return {
value_stream_id: this.selectedValueStream?.id || null, value_stream_id: this.selectedValueStream?.id || null,
...@@ -135,7 +106,8 @@ export default { ...@@ -135,7 +106,8 @@ export default {
created_after: toYmd(this.startDate), created_after: toYmd(this.startDate),
created_before: toYmd(this.endDate), created_before: toYmd(this.endDate),
stage_id: (!this.isOverviewStageSelected && this.selectedStage?.id) || null, // the `overview` stage is always the default, so dont persist the id if its selected stage_id: (!this.isOverviewStageSelected && this.selectedStage?.id) || null, // the `overview` stage is always the default, so dont persist the id if its selected
...stageParams, sort: (!this.isOverviewStageSelected && this.pagination?.sort) || null,
direction: (!this.isOverviewStageSelected && this.pagination?.direction) || null,
}; };
}, },
stageCount() { stageCount() {
...@@ -224,7 +196,7 @@ export default { ...@@ -224,7 +196,7 @@ export default {
/> />
<div v-if="!shouldRenderEmptyState" class="gl-max-w-full"> <div v-if="!shouldRenderEmptyState" class="gl-max-w-full">
<path-navigation <path-navigation
v-if="shouldDisplayPathNavigation" v-if="selectedStageReady"
:key="`path_navigation_key_${pathNavigationData.length}`" :key="`path_navigation_key_${pathNavigationData.length}`"
class="js-path-navigation gl-w-full gl-pb-2" class="js-path-navigation gl-w-full gl-pb-2"
:loading="isLoading" :loading="isLoading"
...@@ -276,65 +248,27 @@ export default { ...@@ -276,65 +248,27 @@ export default {
" "
/> />
<template v-else> <template v-else>
<metrics <template v-if="isOverviewStageSelected">
v-if="!featureFlags.hasPathNavigation || isOverviewStageSelected" <metrics :group-path="currentGroupPath" :request-params="cycleAnalyticsRequestParams" />
:group-path="currentGroupPath" <duration-chart
:request-params="cycleAnalyticsRequestParams" v-if="shouldDisplayDurationChart"
/> class="gl-mt-3"
<template v-if="featureFlags.hasPathNavigation"> :stages="activeStages"
<stage-table-new
v-if="!isLoading && !isOverviewStageSelected"
:is-loading="isLoading || isLoadingStage"
:stage-events="currentStageEvents"
:selected-stage="selectedStage"
:empty-state-message="selectedStageError"
:no-data-svg-path="noDataSvgPath"
:pagination="pagination"
@handleUpdatePagination="onHandleUpdatePagination"
/> />
<type-of-work-charts />
</template> </template>
<stage-table <stage-table-new
v-else v-else
:key="stageCount" :is-loading="isLoading || isLoadingStage"
class="js-stage-table" :stage-events="currentStageEvents"
:current-stage="selectedStage" :selected-stage="selectedStage"
:is-loading="isLoading"
:is-loading-stage="isLoadingStage"
:is-empty-stage="isEmptyStage"
:custom-stage-form-active="customStageFormActive"
:current-stage-events="currentStageEvents"
:no-data-svg-path="noDataSvgPath"
:empty-state-message="selectedStageError" :empty-state-message="selectedStageError"
:has-path-navigation="featureFlags.hasPathNavigation" :no-data-svg-path="noDataSvgPath"
> :pagination="pagination"
<template v-if="shouldDisplayVerticalNavigation" #nav> @handleUpdatePagination="onHandleUpdatePagination"
<stage-table-nav />
:current-stage="selectedStage" <url-sync v-if="selectedStageReady" :query="query" />
:stages="activeStages"
:medians="medians"
:is-creating-custom-stage="isCreatingCustomStage"
:custom-ordering="enableCustomOrdering"
@reorderStage="onStageReorder"
@selectStage="onStageSelect"
@editStage="onShowEditStageForm"
@showAddStageForm="onShowAddStageForm"
@hideStage="onUpdateCustomStage"
@removeStage="onRemoveStage"
/>
</template>
<template v-if="customStageFormActive" #content>
<custom-stage-form
:events="formEvents"
@createStage="onCreateCustomStage"
@updateStage="onUpdateCustomStage"
@clearErrors="$emit('clear-form-errors')"
/>
</template>
</stage-table>
<url-sync :query="query" />
</template> </template>
<duration-chart v-if="shouldDisplayDurationChart" class="gl-mt-3" :stages="activeStages" />
<type-of-work-charts v-if="shouldDisplayTypeOfWorkCharts" />
</div> </div>
</div> </div>
</template> </template>
...@@ -19,10 +19,7 @@ export default () => { ...@@ -19,10 +19,7 @@ export default () => {
const { emptyStateSvgPath, noDataSvgPath, noAccessSvgPath } = el.dataset; const { emptyStateSvgPath, noDataSvgPath, noAccessSvgPath } = el.dataset;
const initialData = buildCycleAnalyticsInitialData(el.dataset); const initialData = buildCycleAnalyticsInitialData(el.dataset);
const store = createStore(); const store = createStore();
const { const { cycleAnalyticsScatterplotEnabled: hasDurationChart = false } = gon?.features;
cycleAnalyticsScatterplotEnabled: hasDurationChart = false,
valueStreamAnalyticsPathNavigation: hasPathNavigation = false,
} = gon?.features;
const { const {
author_username = null, author_username = null,
...@@ -39,7 +36,7 @@ export default () => { ...@@ -39,7 +36,7 @@ export default () => {
selectedMilestone: milestone_title, selectedMilestone: milestone_title,
selectedAssigneeList: assignee_username, selectedAssigneeList: assignee_username,
selectedLabelList: label_name, selectedLabelList: label_name,
featureFlags: { hasDurationChart, hasPathNavigation }, featureFlags: { hasDurationChart },
pagination: { sort: sort?.value || null, direction: direction?.value || null }, pagination: { sort: sort?.value || null, direction: direction?.value || null },
}); });
......
...@@ -172,41 +172,12 @@ export const receiveGroupStagesError = ({ commit }, error) => { ...@@ -172,41 +172,12 @@ export const receiveGroupStagesError = ({ commit }, error) => {
}); });
}; };
export const setDefaultSelectedStage = ({ state: { featureFlags }, dispatch, getters }) => { export const setDefaultSelectedStage = ({ dispatch }) =>
const { activeStages = [] } = getters; dispatch('setSelectedStage', OVERVIEW_STAGE_CONFIG);
if (featureFlags?.hasPathNavigation) { export const receiveGroupStagesSuccess = ({ commit }, stages) =>
return dispatch('setSelectedStage', OVERVIEW_STAGE_CONFIG);
}
if (activeStages?.length) {
const [firstActiveStage] = activeStages;
return Promise.all([
dispatch('setSelectedStage', firstActiveStage),
dispatch('fetchStageData', firstActiveStage.slug),
]);
}
createFlash({
message: __('There was an error while fetching value stream analytics data.'),
});
return Promise.resolve();
};
export const receiveGroupStagesSuccess = (
{ state: { featureFlags }, commit, dispatch },
stages,
) => {
commit(types.RECEIVE_GROUP_STAGES_SUCCESS, stages); commit(types.RECEIVE_GROUP_STAGES_SUCCESS, stages);
if (!featureFlags?.hasPathNavigation) {
return dispatch('setDefaultSelectedStage');
}
return Promise.resolve();
};
export const fetchGroupStagesAndEvents = ({ dispatch, getters }) => { export const fetchGroupStagesAndEvents = ({ dispatch, getters }) => {
const { const {
currentValueStreamId: valueStreamId, currentValueStreamId: valueStreamId,
......
...@@ -51,17 +51,7 @@ export default { ...@@ -51,17 +51,7 @@ export default {
state.medians = {}; state.medians = {};
}, },
[types.RECEIVE_STAGE_MEDIANS_SUCCESS](state, medians = []) { [types.RECEIVE_STAGE_MEDIANS_SUCCESS](state, medians = []) {
if (state?.featureFlags?.hasPathNavigation) { state.medians = formatMedianValuesWithOverview(medians);
state.medians = formatMedianValuesWithOverview(medians);
} else {
state.medians = medians.reduce(
(acc, { id, value, error = null }) => ({
...acc,
[id]: { value, error },
}),
{},
);
}
}, },
[types.RECEIVE_STAGE_MEDIANS_ERROR](state) { [types.RECEIVE_STAGE_MEDIANS_ERROR](state) {
state.medians = {}; state.medians = {};
......
...@@ -14,7 +14,6 @@ class Groups::Analytics::CycleAnalyticsController < Groups::Analytics::Applicati ...@@ -14,7 +14,6 @@ class Groups::Analytics::CycleAnalyticsController < Groups::Analytics::Applicati
before_action do before_action do
push_frontend_feature_flag(:cycle_analytics_scatterplot_enabled, default_enabled: true) push_frontend_feature_flag(:cycle_analytics_scatterplot_enabled, default_enabled: true)
push_frontend_feature_flag(:value_stream_analytics_path_navigation, @group, default_enabled: :yaml)
render_403 unless can?(current_user, :read_group_cycle_analytics, @group) render_403 unless can?(current_user, :read_group_cycle_analytics, @group)
end end
......
---
title: Remove the value_stream_analytics_path_navigation feature flag
merge_request: 60449
author:
type: changed
---
name: value_stream_analytics_path_navigation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31069
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323982
milestone: '13.0'
type: development
group: group::optimize
default_enabled: true
...@@ -42,11 +42,8 @@ RSpec.describe 'Value stream analytics charts', :js do ...@@ -42,11 +42,8 @@ RSpec.describe 'Value stream analytics charts', :js do
context 'Duration chart' do context 'Duration chart' do
duration_stage_selector = '.js-dropdown-stages' duration_stage_selector = '.js-dropdown-stages'
stage_nav_selector = '.stage-nav'
stage_table_selector = '.js-stage-table'
let(:duration_chart_dropdown) { page.find(duration_stage_selector) } let(:duration_chart_dropdown) { page.find(duration_stage_selector) }
let(:first_default_stage) { page.find('.stage-nav-item-cell', text: 'Issue').ancestor('.stage-nav-item') }
let(:custom_value_stream_name) { "New created value stream" } let(:custom_value_stream_name) { "New created value stream" }
let_it_be(:translated_default_stage_names) do let_it_be(:translated_default_stage_names) do
...@@ -93,34 +90,6 @@ RSpec.describe 'Value stream analytics charts', :js do ...@@ -93,34 +90,6 @@ RSpec.describe 'Value stream analytics charts', :js do
expect(duration_chart_stages).not_to include(first_stage_name) expect(duration_chart_stages).not_to include(first_stage_name)
end end
context 'With the path navigation feature flag disabled' do
let(:nav) { page.find(stage_nav_selector) }
before do
stub_feature_flags(value_stream_analytics_path_navigation: false)
select_group(group, stage_table_selector)
end
it_behaves_like 'has all the default stages'
context 'hidden stage' do
before do
toggle_more_options(first_default_stage)
click_button(_('Hide stage'))
end
it 'will not appear in the duration chart dropdown' do
# wait for the stage list to laod
expect(nav).to have_content(s_('CycleAnalyticsStage|Plan'))
toggle_duration_chart_dropdown
expect(duration_chart_stages).not_to include(s_('CycleAnalyticsStage|Issue'))
end
end
end
end end
describe 'Tasks by type chart', :js do describe 'Tasks by type chart', :js do
......
...@@ -41,7 +41,6 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do ...@@ -41,7 +41,6 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do
start_field_label = 'custom-stage-start-event-label-0' start_field_label = 'custom-stage-start-event-label-0'
end_field_label = 'custom-stage-end-event-label-0' end_field_label = 'custom-stage-end-event-label-0'
name_field = 'custom-stage-name-0' name_field = 'custom-stage-name-0'
stage_table_selector = '.js-stage-table'
let(:add_stage_button) { '.js-add-stage-button' } let(:add_stage_button) { '.js-add-stage-button' }
let(:params) { { name: custom_stage_name, start_event_identifier: start_event_identifier, end_event_identifier: end_event_identifier } } let(:params) { { name: custom_stage_name, start_event_identifier: start_event_identifier, end_event_identifier: end_event_identifier } }
...@@ -84,77 +83,6 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do ...@@ -84,77 +83,6 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do
sign_in(user) sign_in(user)
end end
context 'Manual ordering' do
before do
stub_feature_flags(value_stream_analytics_path_navigation: false)
select_group(group, stage_table_selector)
end
let(:default_stage_order) { %w[Issue Plan Code Test Review Staging].freeze }
def confirm_stage_order(stages)
page.within('.stage-nav>ul') do
stages.each_with_index do |stage, index|
expect(find("li:nth-child(#{index + 1})")).to have_content(stage)
end
end
end
context 'with only default stages' do
it 'does not allow stages to be draggable', :js do
confirm_stage_order(default_stage_order)
drag_from_index_to_index(0, 1)
confirm_stage_order(default_stage_order)
end
end
context 'with at least one custom stage', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/216745' do
default_custom_stage_order = %w[Issue Plan Code Test Review Staging Cool\ beans].freeze
stages_near_middle_swapped = %w[Issue Plan Test Code Review Staging Cool\ beans].freeze
stage_dragged_to_top = %w[Review Issue Plan Code Test Staging Cool\ beans].freeze
stage_dragged_to_bottom = %w[Issue Plan Code Test Staging Cool\ beans Review].freeze
shared_examples 'draggable stage' do |original_order, updated_order, start_index, end_index,|
before do
page.driver.browser.manage.window.resize_to(1650, 1150)
create_custom_stage
select_group(group)
end
it 'allows a stage to be dragged' do
confirm_stage_order(original_order)
drag_from_index_to_index(start_index, end_index)
confirm_stage_order(updated_order)
end
it 'persists the order when a group is selected' do
drag_from_index_to_index(start_index, end_index)
select_group(group)
confirm_stage_order(updated_order)
end
end
context 'dragging a stage to the top', :js do
it_behaves_like 'draggable stage', default_custom_stage_order, stage_dragged_to_top, 4, 0
end
context 'dragging a stage to the bottom', :js do
it_behaves_like 'draggable stage', default_custom_stage_order, stage_dragged_to_bottom, 4, 7
end
context 'dragging stages in the middle', :js do
it_behaves_like 'draggable stage', default_custom_stage_order, stages_near_middle_swapped, 2, 3
end
end
end
shared_examples 'submits custom stage form successfully' do |stage_name| shared_examples 'submits custom stage form successfully' do |stage_name|
it 'custom stage is saved with confirmation message' do it 'custom stage is saved with confirmation message' do
fill_in name_field, with: stage_name fill_in name_field, with: stage_name
...@@ -291,217 +219,4 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do ...@@ -291,217 +219,4 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do
end end
end end
end end
context 'With the path navigation feature flag disabled' do
before do
stub_feature_flags(value_stream_analytics_path_navigation: false)
end
context 'with a group' do
context 'selected' do
before do
select_group(group, stage_table_selector)
end
it_behaves_like 'can create custom stages' do
let(:first_label) { group_label1 }
let(:second_label) { group_label2 }
let(:other_label) { label }
end
end
context 'with a custom stage created', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/273045' do
before do
create_custom_stage
select_group(group, stage_table_selector)
expect(page).to have_text custom_stage_name
end
it_behaves_like 'can edit custom stages'
end
end
context 'with a sub group' do
context 'selected' do
before do
select_group(sub_group, stage_table_selector)
end
it_behaves_like 'can create custom stages' do
let(:first_label) { sub_group_label1 }
let(:second_label) { sub_group_label2 }
let(:other_label) { label }
end
end
context 'with a custom stage created' do
before do
create_custom_stage(sub_group)
select_group(sub_group, stage_table_selector)
expect(page).to have_text custom_stage_name
end
it_behaves_like 'can edit custom stages'
end
end
context 'Add a stage button' do
before do
stub_feature_flags(value_stream_analytics_path_navigation: false)
select_group(group, stage_table_selector)
end
it 'displays the custom stage form when clicked' do
expect(page).not_to have_text(s_('CustomCycleAnalytics|New stage'))
expect(page).to have_selector(add_stage_button, visible: true)
expect(page).to have_text(s_('CustomCycleAnalytics|Add a stage'))
expect(page).not_to have_selector("#{add_stage_button}.active")
page.find(add_stage_button).click
expect(page).to have_selector("#{add_stage_button}.active")
expect(page).to have_text(s_('CustomCycleAnalytics|New stage'))
end
end
context 'default stages' do
def open_recover_stage_dropdown
find(add_stage_button).click
click_button(_('Recover hidden stage'))
end
def active_stages
page.all('.stage-nav .stage-name').collect(&:text)
end
before do
stub_feature_flags(value_stream_analytics_path_navigation: false)
select_group(group, stage_table_selector)
toggle_more_options(first_default_stage)
end
it "can be hidden, can't be edited or removed" do
expect(find_stage_actions_btn(first_default_stage)).to have_text(_('Hide stage'))
expect(find_stage_actions_btn(first_default_stage)).not_to have_text(_('Edit stage'))
expect(find_stage_actions_btn(first_default_stage)).not_to have_text(_('Remove stage'))
end
context 'Hide stage' do
before do
click_button(_('Hide stage'))
# wait for the stage list to load
expect(nav).to have_content(s_('CycleAnalyticsStage|Plan'))
end
it 'disappears from the stage table & can be recovered' do
expect(active_stages).not_to include(s_('CycleAnalyticsStage|Issue'))
open_recover_stage_dropdown
expect(page.find("[data-testid='recover-hidden-stage-dropdown']")).to have_text(s_('CycleAnalyticsStage|Issue'))
end
end
context 'Recover stage' do
before do
click_button(_('Hide stage'))
# wait for the stage list to load
expect(nav).to have_content(s_('CycleAnalyticsStage|Plan'))
end
it 'recovers the stage back to the stage table' do
open_recover_stage_dropdown
click_button(s_('CycleAnalyticsStage|Issue'))
# wait for the stage list to load
expect(nav).to have_content(s_('CycleAnalyticsStage|Plan'))
expect(page.find('.flash-notice')).to have_content(_('Stage data updated'))
expect(active_stages).to include(s_('CycleAnalyticsStage|Issue'))
end
end
end
context 'custom stages' do
before do
stub_feature_flags(value_stream_analytics_path_navigation: false)
create_custom_stage
select_group(group, stage_table_selector)
expect(page).to have_text custom_stage_name
toggle_more_options(first_custom_stage)
end
it 'can not be hidden, can be edited or removed' do
expect(find_stage_actions_btn(first_custom_stage)).not_to have_text(_('Hide stage'))
expect(find_stage_actions_btn(first_custom_stage)).to have_text(_('Edit stage'))
expect(find_stage_actions_btn(first_custom_stage)).to have_text(_('Remove stage'))
end
it 'disappears from the stage table after being removed' do
nav = page.find(stage_nav_selector)
expect(nav).to have_text(custom_stage_name)
click_button(_('Remove stage'))
expect(page.find('.flash-notice')).to have_text(_('Stage removed'))
expect(nav).not_to have_text(custom_stage_name)
end
end
end
context 'Duration chart' do
let(:duration_chart_dropdown) { page.find(duration_stage_selector) }
let_it_be(:translated_default_stage_names) do
Gitlab::Analytics::CycleAnalytics::DefaultStages.names.map do |name|
stage = Analytics::CycleAnalytics::GroupStage.new(name: name)
Analytics::CycleAnalytics::StagePresenter.new(stage).title
end.freeze
end
def duration_chart_stages
duration_chart_dropdown.all('.dropdown-item').collect(&:text)
end
def toggle_duration_chart_dropdown
duration_chart_dropdown.click
end
before do
select_group(group)
end
it 'has all the default stages' do
toggle_duration_chart_dropdown
expect(duration_chart_stages).to eq(translated_default_stage_names)
end
context 'hidden stage' do
before do
stub_feature_flags(value_stream_analytics_path_navigation: false)
select_group(group, stage_table_selector)
toggle_more_options(first_default_stage)
click_button(_('Hide stage'))
# wait for the stage list to load
expect(nav).to have_content(s_('CycleAnalyticsStage|Plan'))
end
it 'will not appear in the duration chart dropdown' do
toggle_duration_chart_dropdown
expect(duration_chart_stages).not_to include(s_('CycleAnalyticsStage|Issue'))
end
end
end
end end
...@@ -17,21 +17,6 @@ RSpec.describe 'Group value stream analytics' do ...@@ -17,21 +17,6 @@ RSpec.describe 'Group value stream analytics' do
it 'pushes frontend feature flags' do it 'pushes frontend feature flags' do
visit group_analytics_cycle_analytics_path(group) visit group_analytics_cycle_analytics_path(group)
expect(page).to have_pushed_frontend_feature_flags( expect(page).to have_pushed_frontend_feature_flags(cycleAnalyticsScatterplotEnabled: true)
cycleAnalyticsScatterplotEnabled: true,
valueStreamAnalyticsPathNavigation: true
)
end
context 'when `value_stream_analytics_path_navigation` is disabled for a group' do
before do
stub_feature_flags(value_stream_analytics_path_navigation: false, thing: group)
end
it 'pushes disabled feature flag to the frontend' do
visit group_analytics_cycle_analytics_path(group)
expect(page).to have_pushed_frontend_feature_flags(valueStreamAnalyticsPathNavigation: false)
end
end end
end end
...@@ -172,44 +172,6 @@ RSpec.describe 'Group value stream analytics filters and data', :js do ...@@ -172,44 +172,6 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
end end
end end
context 'with path navigation feature flag disabled' do
before do
stub_feature_flags(value_stream_analytics_path_navigation: false)
select_group(group, '.js-stage-table')
end
it 'does not show the path navigation' do
expect(page).not_to have_selector(path_nav_selector)
end
it 'shows the vertical stage navigation' do
expect(page).to have_selector(stage_nav_selector, visible: true)
end
it 'displays the default list of stages' do
stage_nav = page.find(stage_nav_selector)
%w[Issue Plan Code Test Review Staging].each do |item|
string_id = "CycleAnalytics|#{item}"
expect(stage_nav).to have_content(s_(string_id))
end
end
it 'each stage will have median values', :sidekiq_might_not_need_inline do
stage_medians = page.all('.stage-nav .stage-median').collect(&:text)
expect(stage_medians).to eq(["Not enough data"] * 6)
end
it 'displays the stage table headers' do
expect(page).to have_selector('.stage-header', visible: true)
expect(page).to have_selector('.median-header', visible: true)
expect(page).to have_selector('.event-header', visible: true)
expect(page).to have_selector('.total-time-header', visible: true)
end
end
context 'without valid query parameters set' do context 'without valid query parameters set' do
context 'with created_after date > created_before date' do context 'with created_after date > created_before date' do
before do before do
......
...@@ -3,16 +3,11 @@ import { createLocalVue, shallowMount, mount } from '@vue/test-utils'; ...@@ -3,16 +3,11 @@ import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import axios from 'axios'; import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex'; import Vuex from 'vuex';
import AddStageButton from 'ee/analytics/cycle_analytics/components/add_stage_button.vue';
import Component from 'ee/analytics/cycle_analytics/components/base.vue'; import Component from 'ee/analytics/cycle_analytics/components/base.vue';
import CustomStageForm from 'ee/analytics/cycle_analytics/components/custom_stage_form.vue';
import DurationChart from 'ee/analytics/cycle_analytics/components/duration_chart.vue'; import DurationChart from 'ee/analytics/cycle_analytics/components/duration_chart.vue';
import FilterBar from 'ee/analytics/cycle_analytics/components/filter_bar.vue'; import FilterBar from 'ee/analytics/cycle_analytics/components/filter_bar.vue';
import Metrics from 'ee/analytics/cycle_analytics/components/metrics.vue'; import Metrics from 'ee/analytics/cycle_analytics/components/metrics.vue';
import PathNavigation from 'ee/analytics/cycle_analytics/components/path_navigation.vue'; import PathNavigation from 'ee/analytics/cycle_analytics/components/path_navigation.vue';
import StageNavItem from 'ee/analytics/cycle_analytics/components/stage_nav_item.vue';
import StageTable from 'ee/analytics/cycle_analytics/components/stage_table.vue';
import StageTableNav from 'ee/analytics/cycle_analytics/components/stage_table_nav.vue';
import StageTableNew from 'ee/analytics/cycle_analytics/components/stage_table_new.vue'; import StageTableNew from 'ee/analytics/cycle_analytics/components/stage_table_new.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';
...@@ -55,7 +50,6 @@ const defaultStubs = { ...@@ -55,7 +50,6 @@ const defaultStubs = {
const defaultFeatureFlags = { const defaultFeatureFlags = {
hasDurationChart: true, hasDurationChart: true,
hasPathNavigation: false,
}; };
const [selectedValueStream] = mockData.valueStreams; const [selectedValueStream] = mockData.valueStreams;
...@@ -122,6 +116,7 @@ describe('Value Stream Analytics component', () => { ...@@ -122,6 +116,7 @@ describe('Value Stream Analytics component', () => {
featureFlags = {}, featureFlags = {},
initialState = initialCycleAnalyticsState, initialState = initialCycleAnalyticsState,
props = {}, props = {},
selectedStage = null,
} = options; } = options;
store = createStore(); store = createStore();
...@@ -152,15 +147,16 @@ describe('Value Stream Analytics component', () => { ...@@ -152,15 +147,16 @@ describe('Value Stream Analytics component', () => {
'receiveGroupStagesSuccess', 'receiveGroupStagesSuccess',
mockData.customizableStagesAndEvents.stages, mockData.customizableStagesAndEvents.stages,
); );
if (selectedStage) {
await store.dispatch('setSelectedStage', selectedStage);
await store.dispatch('fetchStageData', selectedStage.slug);
} else {
await store.dispatch('setDefaultSelectedStage');
}
} }
return comp; return comp;
} }
const findStageNavItemAtIndex = (index) =>
wrapper.find(StageTableNav).findAll(StageNavItem).at(index);
const findAddStageButton = () => wrapper.find(AddStageButton);
const displaysProjectsDropdownFilter = (flag) => { const displaysProjectsDropdownFilter = (flag) => {
expect(wrapper.find(ProjectsDropdownFilter).exists()).toBe(flag); expect(wrapper.find(ProjectsDropdownFilter).exists()).toBe(flag);
}; };
...@@ -173,8 +169,8 @@ describe('Value Stream Analytics component', () => { ...@@ -173,8 +169,8 @@ describe('Value Stream Analytics component', () => {
expect(wrapper.find(Metrics).exists()).toBe(flag); expect(wrapper.find(Metrics).exists()).toBe(flag);
}; };
const displaysStageTable = (flag, component = StageTable) => { const displaysStageTable = (flag) => {
expect(wrapper.find(component).exists()).toBe(flag); expect(wrapper.find(StageTableNew).exists()).toBe(flag);
}; };
const displaysDurationChart = (flag) => { const displaysDurationChart = (flag) => {
...@@ -189,10 +185,6 @@ describe('Value Stream Analytics component', () => { ...@@ -189,10 +185,6 @@ describe('Value Stream Analytics component', () => {
expect(wrapper.find(PathNavigation).exists()).toBe(flag); expect(wrapper.find(PathNavigation).exists()).toBe(flag);
}; };
const displaysAddStageButton = (flag) => {
expect(wrapper.find(AddStageButton).exists()).toBe(flag);
};
const displaysFilterBar = (flag) => { const displaysFilterBar = (flag) => {
expect(wrapper.find(FilterBar).exists()).toBe(flag); expect(wrapper.find(FilterBar).exists()).toBe(flag);
}; };
...@@ -205,12 +197,7 @@ describe('Value Stream Analytics component', () => { ...@@ -205,12 +197,7 @@ describe('Value Stream Analytics component', () => {
beforeEach(async () => { beforeEach(async () => {
const { group, ...stateWithoutGroup } = initialCycleAnalyticsState; const { group, ...stateWithoutGroup } = initialCycleAnalyticsState;
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
wrapper = await createComponent({ wrapper = await createComponent({ initialState: stateWithoutGroup });
featureFlags: {
hasPathNavigation: true,
},
initialState: stateWithoutGroup,
});
}); });
afterEach(() => { afterEach(() => {
...@@ -240,17 +227,12 @@ describe('Value Stream Analytics component', () => { ...@@ -240,17 +227,12 @@ describe('Value Stream Analytics component', () => {
it('does not display the stage table', () => { it('does not display the stage table', () => {
displaysStageTable(false); displaysStageTable(false);
displaysStageTable(false, StageTableNew);
}); });
it('does not display the duration chart', () => { it('does not display the duration chart', () => {
displaysDurationChart(false); displaysDurationChart(false);
}); });
it('does not display the add stage button', () => {
displaysAddStageButton(false);
});
it('does not display the path navigation', () => { it('does not display the path navigation', () => {
displaysPathNavigation(false); displaysPathNavigation(false);
}); });
...@@ -265,11 +247,7 @@ describe('Value Stream Analytics component', () => { ...@@ -265,11 +247,7 @@ describe('Value Stream Analytics component', () => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mockRequiredRoutes(mock); mockRequiredRoutes(mock);
wrapper = await createComponent({ wrapper = await createComponent();
featureFlags: {
hasPathNavigation: true,
},
});
await store.dispatch('receiveCycleAnalyticsDataError', { await store.dispatch('receiveCycleAnalyticsDataError', {
response: { status: httpStatusCodes.FORBIDDEN }, response: { status: httpStatusCodes.FORBIDDEN },
...@@ -297,11 +275,6 @@ describe('Value Stream Analytics component', () => { ...@@ -297,11 +275,6 @@ describe('Value Stream Analytics component', () => {
it('does not display the stage table', () => { it('does not display the stage table', () => {
displaysStageTable(false); displaysStageTable(false);
displaysStageTable(false, StageTableNew);
});
it('does not display the add stage button', () => {
displaysAddStageButton(false);
}); });
it('does not display the tasks by type chart', () => { it('does not display the tasks by type chart', () => {
...@@ -312,35 +285,8 @@ describe('Value Stream Analytics component', () => { ...@@ -312,35 +285,8 @@ describe('Value Stream Analytics component', () => {
displaysDurationChart(false); displaysDurationChart(false);
}); });
describe('path navigation', () => { it('does not display the path navigation', () => {
describe('disabled', () => { displaysPathNavigation(false);
it('does not display the path navigation', () => {
displaysPathNavigation(false);
});
});
describe('enabled', () => {
beforeEach(async () => {
wrapper = await createComponent({
withStageSelected: true,
pathNavigationEnabled: true,
});
mock = new MockAdapter(axios);
mockRequiredRoutes(mock);
mock.onAny().reply(httpStatusCodes.FORBIDDEN);
await waitForPromises();
});
afterEach(() => {
mock.restore();
});
it('does not display the path navigation', () => {
displaysPathNavigation(false);
});
});
}); });
}); });
...@@ -348,12 +294,7 @@ describe('Value Stream Analytics component', () => { ...@@ -348,12 +294,7 @@ describe('Value Stream Analytics component', () => {
beforeEach(async () => { beforeEach(async () => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mockRequiredRoutes(mock); mockRequiredRoutes(mock);
wrapper = await createComponent({ wrapper = await createComponent({ withStageSelected: true });
withStageSelected: true,
featureFlags: {
hasPathNavigation: true,
},
});
}); });
afterEach(() => { afterEach(() => {
...@@ -403,11 +344,6 @@ describe('Value Stream Analytics component', () => { ...@@ -403,11 +344,6 @@ describe('Value Stream Analytics component', () => {
it('hides the stage table', () => { it('hides the stage table', () => {
displaysStageTable(false); displaysStageTable(false);
displaysStageTable(false, StageTableNew);
});
it('hides the add stage button', () => {
displaysAddStageButton(false);
}); });
describe('Without the overview stage selected', () => { describe('Without the overview stage selected', () => {
...@@ -416,112 +352,16 @@ describe('Value Stream Analytics component', () => { ...@@ -416,112 +352,16 @@ describe('Value Stream Analytics component', () => {
mockRequiredRoutes(mock); mockRequiredRoutes(mock);
wrapper = await createComponent({ wrapper = await createComponent({
withStageSelected: true, withStageSelected: true,
featureFlags: { selectedStage: mockData.issueStage,
hasPathNavigation: true,
},
}); });
await store.dispatch('setSelectedStage', mockData.issueStage);
await wrapper.vm.$nextTick();
}); });
it('displays the stage table', () => { it('displays the stage table', () => {
displaysStageTable(true, StageTableNew); displaysStageTable(true);
}); });
it('does not display the add stage button', () => { it('displays the path navigation', () => {
displaysAddStageButton(false); displaysPathNavigation(true);
});
});
describe('path navigation', () => {
describe('disabled', () => {
beforeEach(async () => {
wrapper = await createComponent({
withStageSelected: true,
featureFlags: {
hasPathNavigation: false,
},
});
});
it('does not display the path navigation', () => {
displaysPathNavigation(false);
});
describe('StageTable', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
mockRequiredRoutes(mock);
wrapper = await createComponent({
opts: {
stubs: {
StageTable,
StageTableNav,
StageNavItem,
},
},
withStageSelected: true,
});
});
it('has the first stage selected by default', () => {
const first = findStageNavItemAtIndex(0);
const second = findStageNavItemAtIndex(1);
expect(first.props('isActive')).toBe(true);
expect(second.props('isActive')).toBe(false);
});
it('can navigate to different stages', async () => {
findStageNavItemAtIndex(2).trigger('click');
await wrapper.vm.$nextTick();
const first = findStageNavItemAtIndex(0);
const third = findStageNavItemAtIndex(2);
expect(third.props('isActive')).toBe(true);
expect(first.props('isActive')).toBe(false);
});
describe('Add stage button', () => {
beforeEach(async () => {
wrapper = await createComponent({
opts: {
stubs: {
StageTable,
StageTableNav,
AddStageButton,
},
},
withStageSelected: true,
});
});
it('can navigate to the custom stage form', async () => {
expect(wrapper.find(CustomStageForm).exists()).toBe(false);
findAddStageButton().trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.find(CustomStageForm).exists()).toBe(true);
});
});
});
});
describe('enabled', () => {
beforeEach(async () => {
wrapper = await createComponent({
withStageSelected: true,
featureFlags: {
hasPathNavigation: true,
},
});
});
it('displays the path navigation', () => {
displaysPathNavigation(true);
});
}); });
}); });
}); });
...@@ -532,11 +372,7 @@ describe('Value Stream Analytics component', () => { ...@@ -532,11 +372,7 @@ describe('Value Stream Analytics component', () => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mockRequiredRoutes(mock); mockRequiredRoutes(mock);
wrapper = await createComponent({ wrapper = await createComponent();
featureFlags: {
hasPathNavigation: true,
},
});
}); });
afterEach(() => { afterEach(() => {
...@@ -568,7 +404,8 @@ describe('Value Stream Analytics component', () => { ...@@ -568,7 +404,8 @@ describe('Value Stream Analytics component', () => {
mock mock
.onGet(mockData.endpoints.stageData) .onGet(mockData.endpoints.stageData)
.reply(httpStatusCodes.NOT_FOUND, { response: { status: httpStatusCodes.NOT_FOUND } }); .reply(httpStatusCodes.NOT_FOUND, { response: { status: httpStatusCodes.NOT_FOUND } });
await createComponent();
await createComponent({ withStageSelected: true, selectedStage: mockData.issueStage });
await findError('There was an error fetching data for the selected stage'); await findError('There was an error fetching data for the selected stage');
}); });
...@@ -615,8 +452,10 @@ describe('Value Stream Analytics component', () => { ...@@ -615,8 +452,10 @@ describe('Value Stream Analytics component', () => {
value_stream_id: selectedValueStream.id, value_stream_id: selectedValueStream.id,
created_after: toYmd(mockData.startDate), created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate), created_before: toYmd(mockData.endDate),
stage_id: 1, stage_id: null,
project_ids: null, project_ids: null,
sort: null,
direction: null,
}; };
const selectedProjectIds = mockData.selectedProjects.map(({ id }) => getIdFromGraphQLId(id)); const selectedProjectIds = mockData.selectedProjects.map(({ id }) => getIdFromGraphQLId(id));
...@@ -681,7 +520,7 @@ describe('Value Stream Analytics component', () => { ...@@ -681,7 +520,7 @@ describe('Value Stream Analytics component', () => {
created_after: toYmd(mockData.startDate), created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate), created_before: toYmd(mockData.endDate),
project_ids: selectedProjectIds, project_ids: selectedProjectIds,
stage_id: 1, stage_id: null,
}); });
}); });
}); });
...@@ -693,54 +532,12 @@ describe('Value Stream Analytics component', () => { ...@@ -693,54 +532,12 @@ describe('Value Stream Analytics component', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
}); });
it('sets the stage_id url parameter', async () => { it('sets the stage, sort and direction parameters', async () => {
await shouldMergeUrlParams(wrapper, {
...defaultParams,
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
project_ids: null,
stage_id: 2,
});
});
});
describe('with hasPathNavigation=true', () => {
it('does not set the sort and direction parameters', async () => {
wrapper = await createComponent({
featureFlags: {
hasPathNavigation: true,
},
});
await store.dispatch('initializeCycleAnalytics', initialCycleAnalyticsState);
await wrapper.vm.$nextTick();
await shouldMergeUrlParams(wrapper, { await shouldMergeUrlParams(wrapper, {
...defaultParams, ...defaultParams,
created_after: toYmd(mockData.startDate), stage_id: selectedStage.id,
created_before: toYmd(mockData.endDate), direction: PAGINATION_SORT_DIRECTION_DESC,
project_ids: null, sort: PAGINATION_SORT_FIELD_END_EVENT,
});
});
describe('with a stage selected', () => {
beforeEach(async () => {
wrapper = await createComponent({
featureFlags: {
hasPathNavigation: true,
},
});
await store.dispatch('setSelectedStage', selectedStage);
await wrapper.vm.$nextTick();
});
it('sets the stage, sort and direction parameters', async () => {
await shouldMergeUrlParams(wrapper, {
...defaultParams,
stage_id: selectedStage.id,
direction: PAGINATION_SORT_DIRECTION_DESC,
sort: PAGINATION_SORT_FIELD_END_EVENT,
});
}); });
}); });
}); });
......
...@@ -394,140 +394,31 @@ describe('Value Stream Analytics actions', () => { ...@@ -394,140 +394,31 @@ describe('Value Stream Analytics actions', () => {
}); });
describe('receiveGroupStagesSuccess', () => { describe('receiveGroupStagesSuccess', () => {
describe('when the `hasPathNavigation` feature flag is enabled', () => { it(`commits the ${types.RECEIVE_GROUP_STAGES_SUCCESS} mutation'`, () => {
beforeEach(() => { return testAction(
state = { actions.receiveGroupStagesSuccess,
...state, { ...customizableStagesAndEvents.stages },
featureFlags: { state,
...state.featureFlags, [
hasPathNavigation: true, {
}, type: types.RECEIVE_GROUP_STAGES_SUCCESS,
}; payload: { ...customizableStagesAndEvents.stages },
});
it(`commits the ${types.RECEIVE_GROUP_STAGES_SUCCESS} mutation'`, () => {
return testAction(
actions.receiveGroupStagesSuccess,
{ ...customizableStagesAndEvents.stages },
state,
[
{
type: types.RECEIVE_GROUP_STAGES_SUCCESS,
payload: { ...customizableStagesAndEvents.stages },
},
],
[],
);
});
});
describe('when the `hasPathNavigation` feature flag is disabled', () => {
beforeEach(() => {
state = {
...state,
featureFlags: {
...state.featureFlags,
hasPathNavigation: false,
}, },
}; ],
}); [],
);
it(`commits the ${types.RECEIVE_GROUP_STAGES_SUCCESS} mutation and dispatches 'setDefaultSelectedStage`, () => {
return testAction(
actions.receiveGroupStagesSuccess,
{ ...customizableStagesAndEvents.stages },
state,
[
{
type: types.RECEIVE_GROUP_STAGES_SUCCESS,
payload: { ...customizableStagesAndEvents.stages },
},
],
[{ type: 'setDefaultSelectedStage' }],
);
});
}); });
}); });
describe('setDefaultSelectedStage', () => { describe('setDefaultSelectedStage', () => {
describe('when the `hasPathNavigation` feature flag is enabled', () => { it("dispatches the 'setSelectedStage' with the overview stage", () => {
beforeEach(() => { return testAction(
state = { actions.setDefaultSelectedStage,
...state, null,
featureFlags: { state,
...state.featureFlags, [],
hasPathNavigation: true, [{ type: 'setSelectedStage', payload: OVERVIEW_STAGE_CONFIG }],
}, );
};
});
afterEach(() => {
mock.restore();
});
it("dispatches the 'setSelectedStage' with the overview stage", () => {
return testAction(
actions.setDefaultSelectedStage,
null,
state,
[],
[{ type: 'setSelectedStage', payload: OVERVIEW_STAGE_CONFIG }],
);
});
});
describe('when the `hasPathNavigation` feature flag is disabled', () => {
beforeEach(() => {
state = {
...state,
featureFlags: {
...state.featureFlags,
hasPathNavigation: false,
},
};
});
afterEach(() => {
mock.restore();
});
it("dispatches the 'fetchStageData' action", () => {
return testAction(
actions.setDefaultSelectedStage,
null,
state,
[],
[
{ type: 'setSelectedStage', payload: selectedStage },
{ type: 'fetchStageData', payload: selectedStageSlug },
],
);
});
it.each`
data
${[]}
${null}
`('with $data will flash an error', ({ data }) => {
actions.setDefaultSelectedStage(
{ state, getters: { activeStages: data }, dispatch: () => {} },
{},
);
expect(createFlash).toHaveBeenCalledWith({ message: flashErrorMessage });
});
it('will select the first active stage', () => {
return testAction(
actions.setDefaultSelectedStage,
null,
state,
[],
[
{ type: 'setSelectedStage', payload: stages[1] },
{ type: 'fetchStageData', payload: stages[1].slug },
],
);
});
}); });
}); });
......
...@@ -177,42 +177,23 @@ describe('Value Stream Analytics mutations', () => { ...@@ -177,42 +177,23 @@ describe('Value Stream Analytics mutations', () => {
}); });
describe(`${types.RECEIVE_STAGE_MEDIANS_SUCCESS}`, () => { describe(`${types.RECEIVE_STAGE_MEDIANS_SUCCESS}`, () => {
it('sets each id as a key in the median object with the corresponding value and error', () => { beforeEach(() => {
const stateWithData = { state = {
medians: {}, medians: {},
}; };
mutations[types.RECEIVE_STAGE_MEDIANS_SUCCESS](stateWithData, [ mutations[types.RECEIVE_STAGE_MEDIANS_SUCCESS](state, [
{ id: 1, value: 20 }, { id: 1, value: 7580 },
{ id: 2, value: 10 }, { id: 2, value: 434340 },
]); ]);
expect(stateWithData.medians).toEqual({
1: { value: 20, error: null },
2: { value: 10, error: null },
});
}); });
describe('with hasPathNavigation set to true', () => { it('formats each stage median for display in the path navigation', () => {
beforeEach(() => { expect(state.medians).toMatchObject({ 1: '2h', 2: '5d' });
state = { });
featureFlags: { hasPathNavigation: true },
medians: {},
};
mutations[types.RECEIVE_STAGE_MEDIANS_SUCCESS](state, [
{ id: 1, value: 7580 },
{ id: 2, value: 434340 },
]);
});
it('formats each stage median for display in the path navigation', () => {
expect(state.medians).toMatchObject({ 1: '2h', 2: '5d' });
});
it('calculates the overview median', () => { it('calculates the overview median', () => {
expect(state.medians).toMatchObject({ overview: '5d' }); expect(state.medians).toMatchObject({ overview: '5d' });
});
}); });
}); });
......
...@@ -9748,9 +9748,6 @@ msgstr "" ...@@ -9748,9 +9748,6 @@ msgstr ""
msgid "CustomCycleAnalytics|End event label" msgid "CustomCycleAnalytics|End event label"
msgstr "" msgstr ""
msgid "CustomCycleAnalytics|New stage"
msgstr ""
msgid "CustomCycleAnalytics|Stage name already exists" msgid "CustomCycleAnalytics|Stage name already exists"
msgstr "" msgstr ""
...@@ -26710,9 +26707,6 @@ msgstr "" ...@@ -26710,9 +26707,6 @@ msgstr ""
msgid "Reconfigure" msgid "Reconfigure"
msgstr "" msgstr ""
msgid "Recover hidden stage"
msgstr ""
msgid "Recovering projects" msgid "Recovering projects"
msgstr "" msgstr ""
......
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