Commit 52e6c365 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch '323984-remove-legacy-value-stream-form' into 'master'

Remove legacy custom value stream components and docs

See merge request gitlab-org/gitlab!61710
parents 0289efc2 2f83d038
<script>
export default {
props: {
active: {
type: Boolean,
required: true,
},
},
computed: {
activeClass() {
return 'active font-weight-bold border-style-solid border-color-blue-300';
},
inactiveClass() {
return 'bg-transparent border-style-dashed border-color-default';
},
},
};
</script>
<template>
<li
:class="[active ? activeClass : inactiveClass]"
class="js-add-stage-button stage-nav-item mb-1 rounded d-flex justify-content-center border-width-1px"
@click.prevent="$emit('showform')"
>
{{ s__('CustomCycleAnalytics|Add a stage') }}
</li>
</template>
<script>
import {
GlLoadingIcon,
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlSprintf,
GlButton,
} from '@gitlab/ui';
import { isEqual } from 'lodash';
import Vue from 'vue';
import { mapGetters, mapState } from 'vuex';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import { STAGE_ACTIONS } from '../constants';
import { getAllowedEndEvents, getLabelEventsIdentifiers, isLabelEvent } from '../utils';
import { defaultFields, ERRORS, i18n } from './create_value_stream_form/constants';
import CustomStageFormFields from './create_value_stream_form/custom_stage_fields.vue';
import { validateStage, initializeFormData } from './create_value_stream_form/utils';
export default {
components: {
GlLoadingIcon,
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlSprintf,
GlButton,
CustomStageFormFields,
},
props: {
events: {
type: Array,
required: true,
},
},
data() {
return {
labelEvents: getLabelEventsIdentifiers(this.events),
fields: {},
errors: {},
};
},
computed: {
...mapGetters(['hiddenStages']),
...mapState('customStages', [
'isLoading',
'isSavingCustomStage',
'isEditingCustomStage',
'formInitialData',
'formErrors',
]),
hasErrors() {
return (
this.eventMismatchError || Object.values(this.errors).some((errArray) => errArray?.length)
);
},
startEventRequiresLabel() {
return isLabelEvent(this.labelEvents, this.fields.startEventIdentifier);
},
endEventRequiresLabel() {
return isLabelEvent(this.labelEvents, this.fields.endEventIdentifier);
},
isComplete() {
if (this.hasErrors) {
return false;
}
const {
fields: {
name,
startEventIdentifier,
startEventLabelId,
endEventIdentifier,
endEventLabelId,
},
} = this;
const requiredFields = [startEventIdentifier, endEventIdentifier, name];
if (this.startEventRequiresLabel) {
requiredFields.push(startEventLabelId);
}
if (this.endEventRequiresLabel) {
requiredFields.push(endEventLabelId);
}
return requiredFields.every(
(fieldValue) => fieldValue && (fieldValue.length > 0 || fieldValue > 0),
);
},
isDirty() {
return !isEqual(this.fields, this.formInitialData || defaultFields);
},
eventMismatchError() {
const {
fields: { startEventIdentifier = null, endEventIdentifier = null },
} = this;
if (!startEventIdentifier || !endEventIdentifier) return true;
const endEvents = getAllowedEndEvents(this.events, startEventIdentifier);
return !endEvents.length || !endEvents.includes(endEventIdentifier);
},
saveStageText() {
return this.isEditingCustomStage ? i18n.BTN_UPDATE_STAGE : i18n.BTN_ADD_STAGE;
},
formTitle() {
return this.isEditingCustomStage ? i18n.TITLE_EDIT_STAGE : i18n.TITLE_ADD_STAGE;
},
hasHiddenStages() {
return this.hiddenStages.length;
},
},
watch: {
formInitialData(newFields = {}) {
this.fields = {
...defaultFields,
...newFields,
};
},
formErrors(newErrors = {}) {
this.errors = {
...newErrors,
};
},
},
mounted() {
this.resetFields();
},
methods: {
resetFields() {
const { formInitialData, formErrors } = this;
const { fields, errors } = initializeFormData({
fields: formInitialData,
errors: formErrors,
});
this.fields = { ...fields };
this.errors = { ...errors };
},
handleCancel() {
this.resetFields();
this.$emit('cancel');
},
handleSave() {
const data = convertObjectPropsToSnakeCase(this.fields);
if (this.isEditingCustomStage) {
const { id } = this.fields;
this.$emit(STAGE_ACTIONS.UPDATE, { ...data, id });
} else {
this.$emit(STAGE_ACTIONS.CREATE, data);
}
},
hasFieldErrors(key) {
return this.errors[key]?.length > 0;
},
fieldErrorMessage(key) {
return this.errors[key]?.join('\n');
},
handleRecoverStage(id) {
this.$emit(STAGE_ACTIONS.UPDATE, { id, hidden: false });
},
handleUpdateFields({ field, value }) {
this.fields = { ...this.fields, [field]: value };
const newErrors = validateStage({ ...this.fields, custom: true });
newErrors.endEventIdentifier =
this.fields.startEventIdentifier && this.eventMismatchError
? [ERRORS.INVALID_EVENT_PAIRS]
: newErrors.endEventIdentifier;
Vue.set(this, 'errors', newErrors);
},
},
i18n,
};
</script>
<template>
<div v-if="isLoading">
<gl-loading-icon class="mt-4" size="md" />
</div>
<form v-else class="custom-stage-form m-4 gl-mt-0">
<div class="gl-mb-1 gl-display-flex gl-justify-content-space-between gl-align-items-center">
<h4>{{ formTitle }}</h4>
<gl-dropdown
:text="$options.i18n.RECOVER_HIDDEN_STAGE"
data-testid="recover-hidden-stage-dropdown"
right
>
<gl-dropdown-section-header>{{
$options.i18n.RECOVER_STAGE_TITLE
}}</gl-dropdown-section-header>
<template v-if="hasHiddenStages">
<gl-dropdown-item
v-for="stage in hiddenStages"
:key="stage.id"
@click="handleRecoverStage(stage.id)"
>{{ stage.title }}</gl-dropdown-item
>
</template>
<p v-else class="gl-mx-5 gl-my-3">{{ $options.i18n.RECOVER_STAGES_VISIBLE }}</p>
</gl-dropdown>
</div>
<custom-stage-form-fields
:index="0"
:total-stages="1"
:stage="fields"
:errors="errors"
:stage-events="events"
@input="handleUpdateFields"
@select-label="({ field, value }) => handleUpdateFields({ field, value })"
/>
<div>
<gl-button
:disabled="!isDirty"
category="primary"
data-testid="cancel-custom-stage"
@click="handleCancel"
>
{{ $options.i18n.BTN_CANCEL }}
</gl-button>
<gl-button
:disabled="!isComplete || !isDirty"
variant="success"
category="primary"
data-testid="save-custom-stage"
@click="handleSave"
>
<gl-loading-icon v-if="isSavingCustomStage" size="sm" inline />
{{ saveStageText }}
</gl-button>
</div>
<div class="gl-mt-3">
<gl-sprintf
:message="
__(
'%{strongStart}Note:%{strongEnd} Once a custom stage has been added you can re-order stages by dragging them into the desired position.',
)
"
>
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
</gl-sprintf>
</div>
</form>
</template>
......@@ -43,16 +43,6 @@ export const TASKS_BY_TYPE_FILTERS = {
LABEL: 'LABEL',
};
export const STAGE_ACTIONS = {
SELECT: 'selectStage',
EDIT: 'editStage',
REMOVE: 'removeStage',
HIDE: 'hideStage',
CREATE: 'createStage',
UPDATE: 'updateStage',
ADD_STAGE: 'showAddStageForm',
};
export const DEFAULT_VALUE_STREAM_ID = 'default';
export const OVERVIEW_METRICS = {
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Customizable Group Value Stream Analytics', :js do
include DragTo
include CycleAnalyticsHelpers
let_it_be(:group) { create(:group, name: 'CA-test-group') }
let_it_be(:sub_group) { create(:group, name: 'CA-sub-group', parent: group) }
let_it_be(:group2) { create(:group, name: 'CA-bad-test-group') }
let_it_be(:project) { create(:project, :repository, namespace: group, group: group, name: 'Cool fun project') }
let_it_be(:group_label1) { create(:group_label, group: group) }
let_it_be(:group_label2) { create(:group_label, group: group) }
let_it_be(:label) { create(:group_label, group: group2) }
let_it_be(:sub_group_label1) { create(:group_label, group: sub_group) }
let_it_be(:sub_group_label2) { create(:group_label, group: sub_group) }
let_it_be(:user) do
create(:user).tap do |u|
group.add_owner(u)
project.add_maintainer(u)
end
end
let(:milestone) { create(:milestone, project: project) }
let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) }
let(:stage_nav_selector) { '.stage-nav' }
let(:duration_stage_selector) { '.js-dropdown-stages' }
custom_stage_name = 'Cool beans'
custom_stage_with_labels_name = 'Cool beans - now with labels'
start_event_identifier = :merge_request_created
end_event_identifier = :merge_request_merged
start_event_text = "Merge request created"
end_event_text = "Merge request merged"
start_label_event = :issue_label_added
end_label_event = :issue_label_removed
start_event_field = 'custom-stage-start-event-0'
end_event_field = 'custom-stage-end-event-0'
start_field_label = 'custom-stage-start-event-label-0'
end_field_label = 'custom-stage-end-event-label-0'
name_field = 'custom-stage-name-0'
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(:first_default_stage) { page.find('.stage-nav-item-cell', text: 'Issue').ancestor('.stage-nav-item') }
let(:first_custom_stage) { page.find('.stage-nav-item-cell', text: custom_stage_name).ancestor('.stage-nav-item') }
let(:nav) { page.find(stage_nav_selector) }
def create_custom_stage(parent_group = group)
Analytics::CycleAnalytics::Stages::CreateService.new(parent: parent_group, params: params, current_user: user).execute
end
def toggle_more_options(stage)
stage.hover
find_stage_actions_btn(stage).click
end
def select_dropdown_option(name, value = start_event_identifier)
toggle_dropdown name
page.find("[data-testid='#{name}'] .dropdown-menu").all('.dropdown-item').find { |item| item.value == value.to_s }.click
end
def select_dropdown_label(field, index = 1)
page.find("[data-testid='#{field}'] .dropdown-menu").all('.dropdown-item')[index].click
end
def drag_from_index_to_index(from, to)
drag_to(selector: '.stage-nav>ul',
from_index: from,
to_index: to)
end
def find_stage_actions_btn(stage)
stage.find('[data-testid="more-actions-toggle"]')
end
before do
stub_licensed_features(cycle_analytics_for_groups: true, type_of_work_analytics: true)
sign_in(user)
end
shared_examples 'submits custom stage form successfully' do |stage_name|
it 'custom stage is saved with confirmation message' do
fill_in name_field, with: stage_name
click_button(s_('CustomCycleAnalytics|Add stage'))
expect(page.find('.flash-notice')).to have_text(_("Your custom stage '%{title}' was created") % { title: stage_name })
expect(page).to have_selector('.stage-nav-item', text: stage_name)
end
end
shared_examples 'can create custom stages' do
context 'Custom stage form' do
let(:show_form_add_stage_button) { '.js-add-stage-button' }
before do
page.find(show_form_add_stage_button).click
wait_for_requests
end
context 'with empty fields' do
it 'submit button is disabled by default' do
expect(page).to have_button(s_('CustomCycleAnalytics|Add stage'), disabled: true)
end
end
context 'with all required fields set' do
before do
fill_in name_field, with: custom_stage_name
select_dropdown_option start_event_field, start_event_identifier
select_dropdown_option end_event_field, end_event_identifier
end
it 'does not have label dropdowns' do
expect(page).not_to have_content(s_('CustomCycleAnalytics|Start event label'))
expect(page).not_to have_content(s_('CustomCycleAnalytics|End event label'))
end
it 'submit button is disabled if a default name is used' do
fill_in name_field, with: 'issue'
expect(page).to have_button(s_('CustomCycleAnalytics|Add stage'), disabled: true)
end
it 'submit button is disabled if the start event changes' do
select_dropdown_option start_event_field, 'issue_created'
expect(page).to have_button(s_('CustomCycleAnalytics|Add stage'), disabled: true)
end
include_examples 'submits custom stage form successfully', custom_stage_name
end
context 'with label based stages selected' do
before do
fill_in name_field, with: custom_stage_with_labels_name
select_dropdown_option_by_value start_event_field, start_label_event
select_dropdown_option_by_value end_event_field, end_label_event
end
it 'submit button is disabled' do
expect(page).to have_button(s_('CustomCycleAnalytics|Add stage'), disabled: true)
end
context 'with labels available' do
it 'does not contain labels from outside the group' do
toggle_dropdown(start_field_label)
menu = page.find("[data-testid=#{start_field_label}] .dropdown-menu")
expect(menu).not_to have_content(other_label.name)
expect(menu).to have_content(first_label.name)
expect(menu).to have_content(second_label.name)
end
context 'with all required fields set' do
before do
toggle_dropdown(start_field_label)
select_dropdown_label start_field_label, 0
toggle_dropdown(end_field_label)
select_dropdown_label end_field_label, 1
end
include_examples 'submits custom stage form successfully', custom_stage_with_labels_name
end
end
end
end
end
shared_examples 'can edit custom stages' do
context 'Edit stage form' do
let(:stage_form_class) { '.custom-stage-form' }
let(:stage_save_button) { '[data-testid="save-custom-stage"]' }
let(:updated_custom_stage_name) { 'Extra uber cool stage' }
before do
toggle_more_options(first_custom_stage)
click_button(_('Edit stage'))
end
context 'with no changes to the data' do
it 'prepopulates the stage data and disables submit button' do
expect(page.find(stage_form_class)).to have_text(s_('CustomCycleAnalytics|Editing stage'))
expect(page.find("[name='#{name_field}']").value).to eq custom_stage_name
expect(page.find("[data-testid='#{start_event_field}']")).to have_text(start_event_text)
expect(page.find("[data-testid='#{end_event_field}']")).to have_text(end_event_text)
expect(page.find(stage_save_button)[:disabled]).to eq 'true'
end
end
context 'with changes' do
it 'persists updates to the stage' do
fill_in name_field, with: updated_custom_stage_name
page.find(stage_save_button).click
expect(page.find('.flash-notice')).to have_text(_('Stage data updated'))
expect(page.find(stage_nav_selector)).not_to have_text custom_stage_name
expect(page.find(stage_nav_selector)).to have_text updated_custom_stage_name
end
it 'disables the submit form button if incomplete' do
fill_in name_field, with: ''
expect(page.find(stage_save_button)[:disabled]).to eq 'true'
end
it 'doesnt update the stage if a default name is provided' do
fill_in name_field, with: 'issue'
page.find(stage_save_button).click
expect(page.find(stage_form_class)).to have_text(s_('CustomCycleAnalytics|Stage name already exists'))
end
end
end
end
end
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CustomStageForm Editing a custom stage isSavingCustomStage=true displays a loading icon 1`] = `
"<gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\" data-testid=\\"save-custom-stage\\">
<gl-loading-icon-stub label=\\"Loading\\" size=\\"sm\\" color=\\"dark\\" inline=\\"true\\"></gl-loading-icon-stub>
Update stage
</gl-button-stub>"
`;
exports[`CustomStageForm isSavingCustomStage=true displays a loading icon 1`] = `
"<gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\" data-testid=\\"save-custom-stage\\">
<gl-loading-icon-stub label=\\"Loading\\" size=\\"sm\\" color=\\"dark\\" inline=\\"true\\"></gl-loading-icon-stub>
Add stage
</gl-button-stub>"
`;
import { shallowMount } from '@vue/test-utils';
import AddStageButton from 'ee/analytics/cycle_analytics/components/add_stage_button.vue';
describe('AddStageButton', () => {
const active = false;
function createComponent(props) {
return shallowMount(AddStageButton, {
propsData: {
active,
...props,
},
});
}
let wrapper = null;
afterEach(() => {
wrapper.destroy();
});
describe('is not active', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('emits the `showform` event when clicked', () => {
wrapper = createComponent();
expect(wrapper.emitted().showform).toBeUndefined();
wrapper.trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted().showform).toHaveLength(1);
});
});
it('does not have the active class', () => {
expect(wrapper.classes('active')).toBe(false);
});
});
describe('is active', () => {
it('has the active class when active=true', () => {
wrapper = createComponent({ active: true });
expect(wrapper.classes('active')).toBe(true);
});
});
});
import { GlSprintf, GlDropdownItem } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
import CustomStageFields from 'ee/analytics/cycle_analytics/components/create_value_stream_form/custom_stage_fields.vue';
import CustomStageForm from 'ee/analytics/cycle_analytics/components/custom_stage_form.vue';
import { STAGE_ACTIONS } from 'ee/analytics/cycle_analytics/constants';
import customStagesStore from 'ee/analytics/cycle_analytics/store/modules/custom_stages';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import {
endpoints,
groupLabels,
customStageEvents as events,
customStageFormErrors,
} from '../mock_data';
import {
emptyState,
formInitialData,
minimumFields,
MERGE_REQUEST_CREATED,
MERGE_REQUEST_CLOSED,
ISSUE_CREATED,
ISSUE_CLOSED,
} from './create_value_stream_form/mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
const fakeStore = ({ initialState, initialRootGetters }) =>
new Vuex.Store({
getters: {
currentGroupPath: () => 'fake',
hiddenStages: () => [],
...initialRootGetters,
},
modules: {
customStages: {
...customStagesStore,
state: {
isLoading: false,
...initialState,
},
},
},
});
describe('CustomStageForm', () => {
function createComponent({
initialState = {},
initialRootGetters = {},
stubs = {},
props = {},
} = {}) {
return shallowMount(CustomStageForm, {
localVue,
store: fakeStore({ initialState, initialRootGetters }),
propsData: {
events,
...props,
},
stubs: {
GlSprintf,
CustomStageFields,
...stubs,
},
});
}
let wrapper = null;
let mock;
const findEvent = (ev) => wrapper.emitted()[ev];
const findSubmitButton = () => wrapper.find('[data-testid="save-custom-stage"]');
const findCancelButton = () => wrapper.find('[data-testid="cancel-custom-stage"]');
const findRecoverStageDropdown = () =>
wrapper.find('[data-testid="recover-hidden-stage-dropdown"]');
const findFieldErrors = (field) => wrapper.vm.errors[field];
const setFields = async (fields = minimumFields) => {
Object.entries(fields).forEach(([field, value]) => {
wrapper.find(CustomStageFields).vm.$emit('input', { field, value });
});
await wrapper.vm.$nextTick();
};
const setNameField = (value = '') => setFields({ name: value });
const setStartEvent = (value = MERGE_REQUEST_CREATED) =>
setFields({ startEventIdentifier: value });
const setEndEvent = (value = MERGE_REQUEST_CLOSED) => setFields({ endEventIdentifier: value });
const mockGroupLabelsRequest = () =>
new MockAdapter(axios).onGet(endpoints.groupLabels).reply(200, groupLabels);
beforeEach(async () => {
mock = mockGroupLabelsRequest();
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
mock.restore();
});
describe('Default state', () => {
it('will set all fields to null', () => {
expect(wrapper.vm.fields).toMatchObject(emptyState);
});
it('displays the manual ordering helper text', () => {
expect(wrapper.html()).toContain(
'<strong>Note:</strong> Once a custom stage has been added you can re-order stages by dragging them into the desired position.',
);
});
});
describe('Name', () => {
describe('with a reserved name', () => {
beforeEach(async () => {
wrapper = createComponent();
await setNameField('issue');
});
it('displays an error', () => {
expect(findFieldErrors('name')).toContain('Stage name already exists');
});
it('clears the error when the field changes', async () => {
await setNameField('not an issue');
expect(findFieldErrors('name')).toBeUndefined();
});
});
});
describe('End event', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('sets an error if no start event is selected', () => {
expect(findFieldErrors('endEventIdentifier')).toContain('Please select a start event first');
});
it('clears error when a start event is selected', async () => {
await setStartEvent();
expect(findFieldErrors('endEventIdentifier')).not.toContain(
'Please select a start event first',
);
});
describe('with a end event selected and a change to the start event', () => {
beforeEach(async () => {
wrapper = createComponent();
await setFields(minimumFields);
});
it('warns that the start event changed', async () => {
await setStartEvent('');
expect(findFieldErrors('endEventIdentifier')).toContain(
'Please select a start event first',
);
});
it('warns if the current start and end event pair is not valid', async () => {
await setFields({ startEventIdentifier: 'fake_event_id' });
expect(findFieldErrors('endEventIdentifier')).toContain(
'Start event changed, please select a valid end event',
);
});
it('will disable the submit button until a valid endEvent is selected', async () => {
expect(findSubmitButton().props('disabled')).toBe(false);
await setEndEvent('');
expect(findSubmitButton().props('disabled')).toBe(true);
});
});
});
describe('Add stage button', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('has text `Add stage`', () => {
expect(findSubmitButton().text()).toEqual('Add stage');
});
describe('with all fields set', () => {
beforeEach(async () => {
wrapper = createComponent();
await setFields();
});
it('is enabled', () => {
expect(findSubmitButton().props('disabled')).toBe(false);
});
it('does not emit an event until the button is clicked', () => {
expect(findEvent(STAGE_ACTIONS.CREATE)).toBeUndefined();
});
it(`emits a ${STAGE_ACTIONS.CREATE} event when clicked`, async () => {
findSubmitButton().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(findEvent(STAGE_ACTIONS.CREATE)).toHaveLength(1);
});
it(`${STAGE_ACTIONS.CREATE} event receives the latest data`, async () => {
const newData = {
name: 'Cool stage',
start_event_identifier: ISSUE_CREATED,
end_event_identifier: ISSUE_CLOSED,
};
setFields(newData);
findSubmitButton().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(findEvent(STAGE_ACTIONS.CREATE)[0][0]).toMatchObject(newData);
});
});
});
describe('Cancel button', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('is disabled by default', async () => {
expect(findCancelButton().props('disabled')).toBe(true);
});
it('is enabled when the form is dirty', async () => {
await setNameField('Cool stage');
expect(findCancelButton().props('disabled')).toBe(false);
});
it('will reset the fields when clicked', async () => {
await setFields();
findCancelButton().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(wrapper.vm.fields).toMatchObject({
name: null,
startEventIdentifier: null,
startEventLabelId: null,
endEventIdentifier: null,
endEventLabelId: null,
});
});
it('does not emit an event until the button is clicked', () => {
expect(findEvent('cancel')).toBeUndefined();
});
it('will emit the `cancel` event when clicked', async () => {
await setFields();
findCancelButton().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(findEvent('cancel')).toHaveLength(1);
});
});
describe('isSavingCustomStage=true', () => {
beforeEach(async () => {
wrapper = createComponent({
initialState: {
isSavingCustomStage: true,
},
});
await wrapper.vm.$nextTick();
});
it('displays a loading icon', () => {
expect(findSubmitButton().html()).toMatchSnapshot();
});
});
describe('Editing a custom stage', () => {
beforeEach(async () => {
wrapper = createComponent({
initialState: {
isEditingCustomStage: true,
formInitialData,
},
});
});
it('Cancel button will reset the fields to initial state when clicked', async () => {
await setFields(minimumFields);
findCancelButton().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(wrapper.vm.fields).toEqual({ ...formInitialData });
});
describe('Update stage button', () => {
it('has text `Update stage`', () => {
expect(findSubmitButton().text('value')).toEqual('Update stage');
});
it('is disabled by default', () => {
expect(findSubmitButton().props('disabled')).toBe(true);
});
it('is enabled when a field is changed and fields are valid', async () => {
await setFields(minimumFields);
expect(findSubmitButton().props('disabled')).toBe(false);
});
it('is disabled when a field is changed but fields are incomplete', async () => {
await setFields({ name: '' });
expect(findSubmitButton().props('disabled')).toBe(true);
});
it('does not emit an event until the button is clicked', () => {
expect(findEvent(STAGE_ACTIONS.UPDATE)).toBeUndefined();
});
it(`emits a ${STAGE_ACTIONS.UPDATE} event when clicked`, async () => {
await setFields({ name: 'Cool updated form' });
findSubmitButton().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(findEvent(STAGE_ACTIONS.UPDATE)).toHaveLength(1);
});
it('`submit` event receives the latest data', async () => {
await setFields({ name: 'Cool updated form' });
findSubmitButton().vm.$emit('click');
await wrapper.vm.$nextTick();
const submitted = findEvent(STAGE_ACTIONS.UPDATE)[0];
expect(submitted).not.toEqual([formInitialData]);
expect(submitted).toEqual([
convertObjectPropsToSnakeCase({ ...formInitialData, name: 'Cool updated form' }),
]);
});
});
describe('isSavingCustomStage=true', () => {
beforeEach(() => {
wrapper = createComponent({
initialState: { isEditingCustomStage: true, isSavingCustomStage: true },
});
});
it('displays a loading icon', () => {
expect(findSubmitButton().html()).toMatchSnapshot();
});
});
});
describe('With initial errors', () => {
beforeEach(() => {
wrapper = createComponent({
initialState: {
formErrors: customStageFormErrors,
},
});
});
it('renders the errors for the relevant fields', () => {
expect(findFieldErrors('name')).toEqual(['is reserved', 'cant be blank']);
expect(findFieldErrors('startEventIdentifier')).toEqual(['cant be blank']);
});
});
describe('recover stage dropdown', () => {
describe('without hidden stages', () => {
it('has the recover stage dropdown', () => {
expect(findRecoverStageDropdown().exists()).toBe(true);
});
it('has no stages available to recover', async () => {
expect(findRecoverStageDropdown().text()).toContain(
'All default stages are currently visible',
);
});
});
describe('with hidden stages', () => {
beforeEach(() => {
wrapper = createComponent({
initialRootGetters: {
hiddenStages: () => [
{
id: 'my-stage',
title: 'My default stage',
hidden: true,
},
],
},
});
});
it('has stages available to recover', async () => {
const txt = findRecoverStageDropdown().text();
expect(txt).not.toContain('All default stages are currently visible');
expect(txt).toContain('My default stage');
});
it(`emits the ${STAGE_ACTIONS.UPDATE} action when clicking on a stage to recover`, async () => {
findRecoverStageDropdown().find(GlDropdownItem).vm.$emit('click');
await wrapper.vm.$nextTick();
expect(wrapper.emitted()).toEqual({
[STAGE_ACTIONS.UPDATE]: [[{ hidden: false, id: 'my-stage' }]],
});
});
});
});
});
......@@ -860,9 +860,6 @@ msgstr ""
msgid "%{strongStart}Deletes%{strongEnd} source branch"
msgstr ""
msgid "%{strongStart}Note:%{strongEnd} Once a custom stage has been added you can re-order stages by dragging them into the desired position."
msgstr ""
msgid "%{strongStart}Tip:%{strongEnd} You can also checkout merge requests locally by %{linkStart}following these guidelines%{linkEnd}"
msgstr ""
......@@ -9774,24 +9771,6 @@ msgstr ""
msgid "Custom range (UTC)"
msgstr ""
msgid "CustomCycleAnalytics|Add a stage"
msgstr ""
msgid "CustomCycleAnalytics|Add stage"
msgstr ""
msgid "CustomCycleAnalytics|Editing stage"
msgstr ""
msgid "CustomCycleAnalytics|End event label"
msgstr ""
msgid "CustomCycleAnalytics|Stage name already exists"
msgstr ""
msgid "CustomCycleAnalytics|Start event label"
msgstr ""
msgid "Customer Portal"
msgstr ""
......@@ -11902,9 +11881,6 @@ msgstr ""
msgid "Edit sidebar"
msgstr ""
msgid "Edit stage"
msgstr ""
msgid "Edit this file only."
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