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 = { ...@@ -43,16 +43,6 @@ export const TASKS_BY_TYPE_FILTERS = {
LABEL: 'LABEL', 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 DEFAULT_VALUE_STREAM_ID = 'default';
export const OVERVIEW_METRICS = { 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 "" ...@@ -860,9 +860,6 @@ msgstr ""
msgid "%{strongStart}Deletes%{strongEnd} source branch" msgid "%{strongStart}Deletes%{strongEnd} source branch"
msgstr "" 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}" msgid "%{strongStart}Tip:%{strongEnd} You can also checkout merge requests locally by %{linkStart}following these guidelines%{linkEnd}"
msgstr "" msgstr ""
...@@ -9774,24 +9771,6 @@ msgstr "" ...@@ -9774,24 +9771,6 @@ msgstr ""
msgid "Custom range (UTC)" msgid "Custom range (UTC)"
msgstr "" 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" msgid "Customer Portal"
msgstr "" msgstr ""
...@@ -11902,9 +11881,6 @@ msgstr "" ...@@ -11902,9 +11881,6 @@ msgstr ""
msgid "Edit sidebar" msgid "Edit sidebar"
msgstr "" msgstr ""
msgid "Edit stage"
msgstr ""
msgid "Edit this file only." msgid "Edit this file only."
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