diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/components/base.vue b/ee/app/assets/javascripts/analytics/cycle_analytics/components/base.vue index cb8c44711c6aa58bda50ba6cb81551e648b5d74a..7bc4b05546fb72cdd9d39ae20c4eb41d4bc50130 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/components/base.vue +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/components/base.vue @@ -73,7 +73,6 @@ export default { 'selectedValueStream', 'pagination', 'aggregation', - 'isCreatingAggregation', 'groupPath', 'features', 'canEdit', @@ -100,10 +99,10 @@ export default { return Boolean(this.selectedValueStream && !this.aggregation.lastRunAt); }, shouldRenderEmptyState() { - return this.isLoadingValueStreams || (!this.isCreatingAggregation && !this.hasValueStreams); + return this.isLoadingValueStreams || !this.hasValueStreams; }, shouldRenderAggregationWarning() { - return this.isCreatingAggregation || this.isWaitingForNextAggregation; + return this.isWaitingForNextAggregation; }, shouldRenderStageTable() { return !this.isOverviewStageSelected && this.selectedStageEvents.length; @@ -115,8 +114,7 @@ export default { return Boolean( this.enableCustomizableStages && !this.shouldRenderEmptyState && - !this.isLoadingValueStreams && - !this.isCreatingAggregation, + !this.isLoadingValueStreams, ); }, hasDateRangeSet() { diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/store/actions/value_streams.js b/ee/app/assets/javascripts/analytics/cycle_analytics/store/actions/value_streams.js index 02c37750f8098e7bdf79c7099ba4ca3ed4b1d6ee..92d58e449f564d0a6a8411013c01a37edfee9f75 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/store/actions/value_streams.js +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/store/actions/value_streams.js @@ -6,35 +6,24 @@ import { } from 'ee/api/analytics_api'; import * as types from '../mutation_types'; -export const receiveCreateValueStreamSuccess = ({ commit }, valueStream = {}) => { - commit(types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS, valueStream); - commit(types.SET_CREATING_AGGREGATION, true); -}; - -export const createValueStream = ({ commit, dispatch, getters }, data) => { +export const createValueStream = ({ commit, getters }, data) => { const { namespaceRestApiRequestPath } = getters; commit(types.REQUEST_CREATE_VALUE_STREAM); - return apiCreateValueStream(namespaceRestApiRequestPath, data) - .then(({ data: newValueStream }) => dispatch('receiveCreateValueStreamSuccess', newValueStream)) - .catch(({ response } = {}) => { - const { data: { message, payload: { errors } } = null } = response; - commit(types.RECEIVE_CREATE_VALUE_STREAM_ERROR, { message, errors, data }); - }); + return apiCreateValueStream(namespaceRestApiRequestPath, data).catch(({ response } = {}) => { + const { data: { message, payload: { errors } } = null } = response; + commit(types.RECEIVE_CREATE_VALUE_STREAM_ERROR, { message, errors, data }); + }); }; export const updateValueStream = ({ commit, getters }, { id: valueStreamId, ...data }) => { const { namespaceRestApiRequestPath: namespacePath } = getters; commit(types.REQUEST_UPDATE_VALUE_STREAM); - return apiUpdateValueStream({ namespacePath, valueStreamId, data }) - .then(({ data: newValueStream }) => { - return commit(types.RECEIVE_UPDATE_VALUE_STREAM_SUCCESS, newValueStream); - }) - .catch(({ response } = {}) => { - const { data: { message, payload: { errors } } = null } = response; - commit(types.RECEIVE_UPDATE_VALUE_STREAM_ERROR, { message, errors, data }); - }); + return apiUpdateValueStream({ namespacePath, valueStreamId, data }).catch(({ response } = {}) => { + const { data: { message, payload: { errors } } = null } = response; + commit(types.RECEIVE_UPDATE_VALUE_STREAM_ERROR, { message, errors, data }); + }); }; export const deleteValueStream = ({ commit, dispatch, getters }, valueStreamId) => { diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js b/ee/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js index 22768a25223cad936bf538a1bbd9ed5f96ddb1ce..502ff65c703ce302095fcccf218c3a6da6a27b13 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js @@ -7,7 +7,6 @@ export const SET_PREDEFINED_DATE_RANGE = 'SET_PREDEFINED_DATE_RANGE'; export const SET_SELECTED_VALUE_STREAM = 'SET_SELECTED_VALUE_STREAM'; export const SET_PAGINATION = 'SET_PAGINATION'; export const SET_STAGE_EVENTS = 'SET_STAGE_EVENTS'; -export const SET_CREATING_AGGREGATION = 'SET_CREATING_AGGREGATION'; export const REQUEST_VALUE_STREAM_DATA = 'REQUEST_VALUE_STREAM_DATA'; export const RECEIVE_VALUE_STREAM_DATA_SUCCESS = 'RECEIVE_VALUE_STREAM_DATA_SUCCESS'; @@ -37,11 +36,9 @@ export const INITIALIZE_VSA = 'INITIALIZE_VSA'; export const INITIALIZE_VALUE_STREAM_SUCCESS = 'INITIALIZE_VALUE_STREAM_SUCCESS'; export const REQUEST_CREATE_VALUE_STREAM = 'REQUEST_CREATE_VALUE_STREAM'; -export const RECEIVE_CREATE_VALUE_STREAM_SUCCESS = 'RECEIVE_CREATE_VALUE_STREAM_SUCCESS'; export const RECEIVE_CREATE_VALUE_STREAM_ERROR = 'RECEIVE_CREATE_VALUE_STREAM_ERROR'; export const REQUEST_UPDATE_VALUE_STREAM = 'REQUEST_UPDATE_VALUE_STREAM'; -export const RECEIVE_UPDATE_VALUE_STREAM_SUCCESS = 'RECEIVE_UPDATE_VALUE_STREAM_SUCCESS'; export const RECEIVE_UPDATE_VALUE_STREAM_ERROR = 'RECEIVE_UPDATE_VALUE_STREAM_ERROR'; export const REQUEST_DELETE_VALUE_STREAM = 'REQUEST_DELETE_VALUE_STREAM'; diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js b/ee/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js index c3b4d556e0432e6e187275f8484d2ca06b539818..e3e57e1b55b335629394b1bf1a269fc352dd317d 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js @@ -150,35 +150,18 @@ export default { state.isLoading = false; }, [types.REQUEST_CREATE_VALUE_STREAM](state) { - state.isCreatingValueStream = true; state.createValueStreamErrors = {}; }, [types.RECEIVE_CREATE_VALUE_STREAM_ERROR](state, { data: { stages = [] }, errors = {} }) { const { stages: stageErrors = {}, ...rest } = errors; state.createValueStreamErrors = { ...rest, stages: prepareStageErrors(stages, stageErrors) }; - state.isCreatingValueStream = false; - }, - [types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS](state, valueStream = {}) { - state.isCreatingValueStream = false; - state.createValueStreamErrors = {}; - state.selectedValueStream = convertObjectPropsToCamelCase(valueStream, { deep: true }); - - const { stages = [] } = valueStream; - state.stages = transformRawStages(stages); }, [types.REQUEST_UPDATE_VALUE_STREAM](state) { - state.isEditingValueStream = true; state.createValueStreamErrors = {}; }, [types.RECEIVE_UPDATE_VALUE_STREAM_ERROR](state, { data: { stages = [] }, errors = {} }) { const { stages: stageErrors = {}, ...rest } = errors; state.createValueStreamErrors = { ...rest, stages: prepareStageErrors(stages, stageErrors) }; - state.isEditingValueStream = false; - }, - [types.RECEIVE_UPDATE_VALUE_STREAM_SUCCESS](state, valueStream) { - state.isEditingValueStream = false; - state.createValueStreamErrors = {}; - state.selectedValueStream = convertObjectPropsToCamelCase(valueStream, { deep: true }); }, [types.REQUEST_DELETE_VALUE_STREAM](state) { state.isDeletingValueStream = true; @@ -222,7 +205,4 @@ export default { direction: direction || PAGINATION_SORT_DIRECTION_DESC, }; }, - [types.SET_CREATING_AGGREGATION](state, value) { - state.isCreatingAggregation = value; - }, }; diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/store/state.js b/ee/app/assets/javascripts/analytics/cycle_analytics/store/state.js index 37916446dc894c890c738862c933f076d9d0e471..98fe4c59f0d05a0e864e30fba5e33483a3e62110 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/store/state.js +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/store/state.js @@ -26,11 +26,8 @@ export default () => ({ selectedStageEvents: [], isLoadingValueStreams: false, - isCreatingValueStream: false, - isEditingValueStream: false, isDeletingValueStream: false, isFetchingGroupLabels: false, - isCreatingAggregation: false, isFetchingGroupStagesAndEvents: false, createValueStreamErrors: {}, diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form.stories.js b/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form.stories.js index 5c3b665a29ebd7682225ad7840952a74a24724d8..b5248877a560bc9eceba7f08eb5e136a678b201f 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form.stories.js +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form.stories.js @@ -78,27 +78,6 @@ export const EditValueStreamWithHiddenStages = { }, }; -export const SavingNewValueStream = { - render: createStoryWithState({ - state: { - isCreatingValueStream: true, - }, - }), -}; - -export const UpdatingValueStream = { - render: createStoryWithState({ - state: { - selectedValueStream, - stages: selectedValueStreamStages(), - isEditingValueStream: true, - }, - }), - args: { - isEditing: true, - }, -}; - export const WithFormSubmissionErrors = { render: createStoryWithState({ state: { diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form.vue b/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form.vue index e2b68c970837ef8244e62da2f2215931cf9ad955..070f14e91c886eb024a8608ca7ec8dc62d88cb63 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form.vue +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form.vue @@ -2,7 +2,6 @@ import { GlLoadingIcon } from '@gitlab/ui'; // eslint-disable-next-line no-restricted-imports import { mapState, mapActions } from 'vuex'; -import { mergeUrlParams } from '~/lib/utils/url_utility'; import { generateInitialStageData } from '../utils'; import ValueStreamFormContent from './value_stream_form_content.vue'; @@ -12,11 +11,6 @@ export default { ValueStreamFormContent, GlLoadingIcon, }, - inject: { - vsaPath: { - default: null, - }, - }, props: { isEditing: { type: Boolean, @@ -25,44 +19,29 @@ export default { }, }, computed: { - ...mapState({ - selectedValueStream: 'selectedValueStream', - selectedValueStreamStages: 'stages', - initialFormErrors: 'createValueStreamErrors', - defaultStageConfig: 'defaultStageConfig', - defaultGroupLabels: 'defaultGroupLabels', - isFetchingGroupStagesAndEvents: 'isFetchingGroupStagesAndEvents', - isFetchingGroupLabels: 'isFetchingGroupLabels', - isValueStreamLoading: 'isLoading', - }), - isLoading() { - return ( - this.isValueStreamLoading || - this.isFetchingGroupLabels || - this.isFetchingGroupStagesAndEvents - ); + ...mapState([ + 'selectedValueStream', + 'stages', + 'defaultStageConfig', + 'defaultGroupLabels', + 'isFetchingGroupStagesAndEvents', + 'isFetchingGroupLabels', + 'isLoading', + ]), + isLoadingOrFetching() { + return this.isLoading || this.isFetchingGroupLabels || this.isFetchingGroupStagesAndEvents; }, initialData() { return this.isEditing ? { ...this.selectedValueStream, - stages: generateInitialStageData( - this.defaultStageConfig, - this.selectedValueStreamStages, - ), + stages: generateInitialStageData(this.defaultStageConfig, this.stages), } : { name: '', stages: [], }; }, - valueStreamPath() { - const { selectedValueStream, vsaPath } = this; - - return selectedValueStream - ? mergeUrlParams({ value_stream_id: selectedValueStream.id }, vsaPath) - : vsaPath; - }, }, created() { if (!this.defaultGroupLabels) { @@ -76,16 +55,14 @@ export default { </script> <template> <div> - <div v-if="isLoading" class="gl-pt-7 gl-text-center"> + <div v-if="isLoadingOrFetching" class="gl-pt-7 gl-text-center"> <gl-loading-icon size="lg" /> </div> <value-stream-form-content v-else :initial-data="initialData" - :initial-form-errors="initialFormErrors" :default-stage-config="defaultStageConfig" :is-editing="isEditing" - :value-stream-path="valueStreamPath" /> </div> </template> diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content.vue b/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content.vue index 11e4b2b3f2bab2b50a6bc5a42259844c50fd68c8..1c03c1198f5df2e853fe348cab7437b7af6e7011 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content.vue +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content.vue @@ -7,7 +7,7 @@ import { filterStagesByHiddenStatus } from '~/analytics/cycle_analytics/utils'; import { swapArrayItems } from '~/lib/utils/array_utility'; import { sprintf } from '~/locale'; import Tracking from '~/tracking'; -import { visitUrlWithAlerts } from '~/lib/utils/url_utility'; +import { visitUrlWithAlerts, mergeUrlParams } from '~/lib/utils/url_utility'; import { getLabelEventsIdentifiers } from 'ee/analytics/cycle_analytics/utils'; import { validateValueStreamName, @@ -59,22 +59,13 @@ export default { ValueStreamFormContentActions, }, mixins: [Tracking.mixin()], + inject: ['vsaPath'], props: { initialData: { type: Object, required: false, default: () => ({}), }, - initialPreset: { - type: String, - required: false, - default: PRESET_OPTIONS_DEFAULT, - }, - initialFormErrors: { - type: Object, - required: false, - default: () => ({}), - }, defaultStageConfig: { type: Array, required: true, @@ -84,60 +75,44 @@ export default { required: false, default: false, }, - valueStreamPath: { - type: String, - required: true, - }, }, data() { const { defaultStageConfig = [], initialData: { name: initialName, stages: initialStages = [] }, - initialFormErrors, - initialPreset, } = this; - const { name: nameErrors = [], stages: stageErrors = [{}] } = initialFormErrors; - const additionalFields = { - stages: this.isEditing - ? initializeEditingStages(initialStages) - : initializeStages(defaultStageConfig, initialPreset), - stageErrors: - cloneDeep(stageErrors) || initializeStageErrors(defaultStageConfig, initialPreset), - }; return { hiddenStages: filterStagesByHiddenStatus(initialStages), - selectedPreset: initialPreset, + selectedPreset: PRESET_OPTIONS_DEFAULT, presetOptions: PRESET_OPTIONS, name: initialName, - nameErrors, - stageErrors, + nameErrors: [], + stageErrors: [{}], showSubmitError: false, - isRedirecting: false, - ...additionalFields, + isSubmitting: false, + stages: this.isEditing + ? initializeEditingStages(initialStages) + : initializeStages(defaultStageConfig), }; }, computed: { - ...mapState({ - isCreating: 'isCreatingValueStream', - isSaving: 'isEditingValueStream', - isFetchingGroupLabels: 'isFetchingGroupLabels', - formEvents: 'formEvents', - defaultGroupLabels: 'defaultGroupLabels', - }), + ...mapState([ + 'isFetchingGroupLabels', + 'formEvents', + 'defaultGroupLabels', + 'createValueStreamErrors', + 'selectedValueStream', + ]), + selectedValueStreamId() { + return this.selectedValueStream?.id || -1; + }, isValueStreamNameValid() { return !this.nameErrors?.length; }, invalidNameFeedback() { return this.nameErrors?.length ? this.nameErrors.join('\n\n') : null; }, - hasInitialFormErrors() { - const { initialFormErrors } = this; - return Boolean(Object.keys(initialFormErrors).length); - }, - isSubmitting() { - return this.isCreating || this.isSaving; - }, hasFormErrors() { return Boolean( this.nameErrors.length || this.stageErrors.some((obj) => Object.keys(obj).length), @@ -168,12 +143,18 @@ export default { return { id, message, variant: 'success' }; }, }, + watch: { + createValueStreamErrors: 'refreshErrors', + }, + created() { + this.refreshErrors(); + }, methods: { ...mapActions(['createValueStream', 'updateValueStream']), - onSubmit() { + async onSubmit() { this.showSubmitError = false; this.validate(); - if (this.hasFormErrors) return false; + if (this.hasFormErrors) return; let req = this.createValueStream; let params = { @@ -188,27 +169,22 @@ export default { }; } - return req(params).then(() => { - if (this.hasInitialFormErrors) { - const { name: nameErrors = [], stages: stageErrors = [{}] } = this.initialFormErrors; + this.isSubmitting = true; - this.isRedirecting = false; - this.nameErrors = nameErrors; - this.stageErrors = stageErrors; - this.showSubmitError = true; + const response = await req(params); - return; - } - - this.nameErrors = []; - this.stageErrors = initializeStageErrors(this.defaultStageConfig, this.selectedPreset); - this.track('submit_form', { - label: this.isEditing ? 'edit_value_stream' : 'create_value_stream', - }); - this.isRedirecting = true; + if (this.hasFormErrors) { + this.isSubmitting = false; + this.showSubmitError = true; + return; + } - visitUrlWithAlerts(this.valueStreamPath, [this.submissionSuccessfulAlert]); + this.track('submit_form', { + label: this.isEditing ? 'edit_value_stream' : 'create_value_stream', }); + + const redirectPath = mergeUrlParams({ value_stream_id: response.data.id }, this.vsaPath); + visitUrlWithAlerts(redirectPath, [this.submissionSuccessfulAlert]); }, stageGroupLabel(index) { return sprintf(this.$options.i18n.STAGE_INDEX, { index: index + 1 }); @@ -228,6 +204,13 @@ export default { }), ); }, + refreshErrors() { + const { defaultStageConfig, selectedPreset, createValueStreamErrors = {} } = this; + const { name = [], stages = [{}] } = createValueStreamErrors; + this.nameErrors = name; + this.stageErrors = + cloneDeep(stages) || initializeStageErrors(defaultStageConfig, selectedPreset); + }, validate() { const { name } = this; this.nameErrors = validateValueStreamName({ name }); @@ -437,8 +420,8 @@ export default { <hr class="gl-mb-5 gl-mt-2" /> <value-stream-form-content-actions :is-editing="isEditing" - :is-loading="isSubmitting || isRedirecting" - :value-stream-path="valueStreamPath" + :is-loading="isSubmitting" + :value-stream-id="selectedValueStreamId" @clickPrimaryAction="onSubmit" @clickAddStageAction="onAddStage" /> diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content_actions.vue b/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content_actions.vue index 945fd094e43f47fc54aa40f499af907dd5782f86..9cf398cc8920b8afa44d708d9eb51e9b44b3c439 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content_actions.vue +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content_actions.vue @@ -1,5 +1,6 @@ <script> import { GlButton } from '@gitlab/ui'; +import { mergeUrlParams } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; import { i18n } from '../constants'; @@ -10,22 +11,23 @@ export default { components: { GlButton, }, + inject: ['vsaPath'], props: { isEditing: { type: Boolean, required: false, default: false, }, + valueStreamId: { + type: Number, + required: false, + default: -1, + }, isLoading: { type: Boolean, required: false, default: false, }, - valueStreamPath: { - type: String, - required: false, - default: null, - }, }, i18n: { newValueStreamAction: FORM_TITLE, @@ -39,6 +41,11 @@ export default { ? this.$options.i18n.saveValueStreamAction : this.$options.i18n.newValueStreamAction; }, + cancelHref() { + return this.isEditing && this.valueStreamId > 0 + ? mergeUrlParams({ value_stream_id: this.valueStreamId }, this.vsaPath) + : this.vsaPath; + }, }, }; </script> @@ -60,7 +67,7 @@ export default { @click="$emit('clickAddStageAction')" >{{ $options.i18n.addStageAction }}</gl-button > - <gl-button data-testid="cancel-button" :href="valueStreamPath" :disabled="isLoading">{{ + <gl-button data-testid="cancel-button" :href="cancelHref" :disabled="isLoading">{{ $options.i18n.cancelAction }}</gl-button> </div> diff --git a/ee/spec/frontend/analytics/cycle_analytics/components/value_stream_select_spec.js b/ee/spec/frontend/analytics/cycle_analytics/components/value_stream_select_spec.js index df33e2e31cf342ca8b72c64a9204b2d2536a226a..a0c05ecf58bcdc35bdff7f92b91b8f9b10e5eb4a 100644 --- a/ee/spec/frontend/analytics/cycle_analytics/components/value_stream_select_spec.js +++ b/ee/spec/frontend/analytics/cycle_analytics/components/value_stream_select_spec.js @@ -35,7 +35,6 @@ describe('ValueStreamSelect', () => { const fakeStore = ({ initialState = {} }) => new Vuex.Store({ state: { - isCreatingValueStream: false, isDeletingValueStream: false, createValueStreamErrors: {}, deleteValueStreamError: null, diff --git a/ee/spec/frontend/analytics/cycle_analytics/store/actions/value_streams_spec.js b/ee/spec/frontend/analytics/cycle_analytics/store/actions/value_streams_spec.js index 420f391f56cda85d7677ddd8f07d42ab7da563d2..fb111a343875c766c3907e4881ed585c9c09c491 100644 --- a/ee/spec/frontend/analytics/cycle_analytics/store/actions/value_streams_spec.js +++ b/ee/spec/frontend/analytics/cycle_analytics/store/actions/value_streams_spec.js @@ -85,18 +85,12 @@ describe('Value Stream Analytics actions / value streams', () => { mock.onPost(endpoints.valueStreamData).replyOnce(HTTP_STATUS_OK, createResp); }); - it(`commits the ${types.REQUEST_CREATE_VALUE_STREAM} and ${types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS} actions`, () => { - return testAction( - actions.createValueStream, - payload, - state, - [ - { - type: types.REQUEST_CREATE_VALUE_STREAM, - }, - ], - [{ type: 'receiveCreateValueStreamSuccess', payload: createResp }], - ); + it(`commits ${types.REQUEST_CREATE_VALUE_STREAM}`, () => { + return testAction(actions.createValueStream, payload, state, [ + { + type: types.REQUEST_CREATE_VALUE_STREAM, + }, + ]); }); }); @@ -126,24 +120,6 @@ describe('Value Stream Analytics actions / value streams', () => { }); }); - describe('receiveCreateValueStreamSuccess', () => { - beforeEach(() => { - state = { ...state, valueStream: {} }; - }); - - it(`will dispatch the "fetchCycleAnalyticsData" action and commit the ${types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS} mutation`, () => { - return testAction({ - action: actions.receiveCreateValueStreamSuccess, - payload: selectedValueStream, - state, - expectedMutations: [ - { type: types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS, payload: selectedValueStream }, - { type: types.SET_CREATING_AGGREGATION, payload: true }, - ], - }); - }); - }); - describe('updateValueStream', () => { const payload = { name: 'cool value stream', @@ -164,10 +140,9 @@ describe('Value Stream Analytics actions / value streams', () => { mock.onPut(endpoints.valueStreamData).replyOnce(HTTP_STATUS_OK, updateResp); }); - it(`commits the ${types.REQUEST_UPDATE_VALUE_STREAM} and ${types.RECEIVE_UPDATE_VALUE_STREAM_SUCCESS} mutations`, () => { + it(`commits the ${types.REQUEST_UPDATE_VALUE_STREAM} mutation`, () => { return testAction(actions.updateValueStream, payload, state, [ { type: types.REQUEST_UPDATE_VALUE_STREAM }, - { type: types.RECEIVE_UPDATE_VALUE_STREAM_SUCCESS, payload: updateResp }, ]); }); }); diff --git a/ee/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js b/ee/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js index 9ecefaaee511296092f249b543fc8edeacd65c8b..37ecab58eaf3f1d6d64b5f600d5ebc0a9ad37bf9 100644 --- a/ee/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js +++ b/ee/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js @@ -53,14 +53,8 @@ describe('Value Stream Analytics mutations', () => { ${types.REQUEST_GROUP_STAGES} | ${'stages'} | ${[]} ${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}} ${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}} - ${types.REQUEST_CREATE_VALUE_STREAM} | ${'isCreatingValueStream'} | ${true} - ${types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS} | ${'isCreatingValueStream'} | ${false} ${types.REQUEST_CREATE_VALUE_STREAM} | ${'createValueStreamErrors'} | ${{}} - ${types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS} | ${'createValueStreamErrors'} | ${{}} - ${types.REQUEST_UPDATE_VALUE_STREAM} | ${'isEditingValueStream'} | ${true} - ${types.RECEIVE_UPDATE_VALUE_STREAM_SUCCESS} | ${'isEditingValueStream'} | ${false} ${types.REQUEST_UPDATE_VALUE_STREAM} | ${'createValueStreamErrors'} | ${{}} - ${types.RECEIVE_UPDATE_VALUE_STREAM_SUCCESS} | ${'createValueStreamErrors'} | ${{}} ${types.REQUEST_DELETE_VALUE_STREAM} | ${'isDeletingValueStream'} | ${true} ${types.RECEIVE_DELETE_VALUE_STREAM_SUCCESS} | ${'isDeletingValueStream'} | ${false} ${types.REQUEST_DELETE_VALUE_STREAM} | ${'deleteValueStreamError'} | ${null} @@ -95,22 +89,20 @@ describe('Value Stream Analytics mutations', () => { const pagination = { page: 10, hasNextPage: true, sort: null, direction: null }; it.each` - mutation | payload | expectedState - ${types.SET_SELECTED_PROJECTS} | ${selectedProjects} | ${{ selectedProjects }} - ${types.SET_DATE_RANGE} | ${{ createdAfter, createdBefore }} | ${{ createdAfter, createdBefore }} - ${types.SET_PREDEFINED_DATE_RANGE} | ${predefinedDateRange} | ${{ predefinedDateRange }} - ${types.SET_SELECTED_STAGE} | ${{ id: 'first-stage' }} | ${{ selectedStage: { id: 'first-stage' } }} - ${types.RECEIVE_CREATE_VALUE_STREAM_ERROR} | ${valueStreamErrors} | ${{ createValueStreamErrors: expectedValueStreamErrors, isCreatingValueStream: false }} - ${types.RECEIVE_UPDATE_VALUE_STREAM_ERROR} | ${valueStreamErrors} | ${{ createValueStreamErrors: expectedValueStreamErrors, isEditingValueStream: false }} - ${types.RECEIVE_DELETE_VALUE_STREAM_ERROR} | ${'Some error occurred'} | ${{ deleteValueStreamError: 'Some error occurred' }} - ${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${valueStreams} | ${{ valueStreams, isLoadingValueStreams: false }} - ${types.SET_SELECTED_VALUE_STREAM} | ${valueStreams[1].id} | ${{ selectedValueStream: {} }} - ${types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS} | ${valueStreams[1]} | ${{ selectedValueStream: valueStreams[1] }} - ${types.RECEIVE_UPDATE_VALUE_STREAM_SUCCESS} | ${valueStreams[1]} | ${{ selectedValueStream: valueStreams[1] }} - ${types.RECEIVE_GROUP_LABELS_SUCCESS} | ${groupLabels} | ${{ defaultGroupLabels: groupLabels }} - ${types.SET_PAGINATION} | ${pagination} | ${{ pagination: { ...pagination, sort: PAGINATION_SORT_FIELD_DURATION, direction: PAGINATION_SORT_DIRECTION_DESC } }} - ${types.SET_PAGINATION} | ${{ ...pagination, sort: 'duration', direction: 'asc' }} | ${{ pagination: { ...pagination, sort: 'duration', direction: 'asc' } }} - ${types.SET_STAGE_EVENTS} | ${rawCustomStageEvents} | ${{ formEvents: camelCasedStageEvents }} + mutation | payload | expectedState + ${types.SET_SELECTED_PROJECTS} | ${selectedProjects} | ${{ selectedProjects }} + ${types.SET_DATE_RANGE} | ${{ createdAfter, createdBefore }} | ${{ createdAfter, createdBefore }} + ${types.SET_PREDEFINED_DATE_RANGE} | ${predefinedDateRange} | ${{ predefinedDateRange }} + ${types.SET_SELECTED_STAGE} | ${{ id: 'first-stage' }} | ${{ selectedStage: { id: 'first-stage' } }} + ${types.RECEIVE_CREATE_VALUE_STREAM_ERROR} | ${valueStreamErrors} | ${{ createValueStreamErrors: expectedValueStreamErrors }} + ${types.RECEIVE_UPDATE_VALUE_STREAM_ERROR} | ${valueStreamErrors} | ${{ createValueStreamErrors: expectedValueStreamErrors }} + ${types.RECEIVE_DELETE_VALUE_STREAM_ERROR} | ${'Some error occurred'} | ${{ deleteValueStreamError: 'Some error occurred' }} + ${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${valueStreams} | ${{ valueStreams, isLoadingValueStreams: false }} + ${types.SET_SELECTED_VALUE_STREAM} | ${valueStreams[1].id} | ${{ selectedValueStream: {} }} + ${types.RECEIVE_GROUP_LABELS_SUCCESS} | ${groupLabels} | ${{ defaultGroupLabels: groupLabels }} + ${types.SET_PAGINATION} | ${pagination} | ${{ pagination: { ...pagination, sort: PAGINATION_SORT_FIELD_DURATION, direction: PAGINATION_SORT_DIRECTION_DESC } }} + ${types.SET_PAGINATION} | ${{ ...pagination, sort: 'duration', direction: 'asc' }} | ${{ pagination: { ...pagination, sort: 'duration', direction: 'asc' } }} + ${types.SET_STAGE_EVENTS} | ${rawCustomStageEvents} | ${{ formEvents: camelCasedStageEvents }} `( '$mutation with payload $payload will update state with $expectedState', ({ mutation, payload, expectedState }) => { diff --git a/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content_actions_spec.js b/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content_actions_spec.js index 63fbb36f6a4dd9caa68468dd6769a3b5c857287a..8fbb8f30648036d1e9d734ab892bc42519f48ea1 100644 --- a/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content_actions_spec.js +++ b/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content_actions_spec.js @@ -1,79 +1,84 @@ -import ValueStreamFormContentHeader from 'ee/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content_actions.vue'; +import ValueStreamFormContentActions from 'ee/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content_actions.vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { valueStreamPath } from '../../mock_data'; describe('ValueStreamFormContentActions', () => { + const vsaPath = '/mockVsaPath/test'; + let wrapper; const findPrimaryBtn = () => wrapper.findByTestId('primary-button'); - const findValueStreamCancelBtn = () => wrapper.findByTestId('cancel-button'); + const findCancelBtn = () => wrapper.findByTestId('cancel-button'); const findAddStageBtn = () => wrapper.findByTestId('add-button'); const createComponent = ({ props = {} } = {}) => { - wrapper = shallowMountExtended(ValueStreamFormContentHeader, { + wrapper = shallowMountExtended(ValueStreamFormContentActions, { + provide: { vsaPath }, propsData: { - valueStreamPath, ...props, }, }); }; describe.each` - isEditing | text - ${false} | ${'New value stream'} - ${true} | ${'Save value stream'} - `('when `isEditing` is `$isEditing`', ({ isEditing, text }) => { - beforeEach(() => { - createComponent({ props: { isEditing } }); - }); - - it('renders primary action correctly', () => { - expect(findPrimaryBtn().text()).toBe(text); - expect(findPrimaryBtn().props()).toMatchObject({ - variant: 'confirm', - loading: false, - disabled: false, + isEditing | valueStreamId | text | cancelHref + ${false} | ${-1} | ${'New value stream'} | ${vsaPath} + ${true} | ${-1} | ${'Save value stream'} | ${vsaPath} + ${true} | ${13} | ${'Save value stream'} | ${`/mockVsaPath/test?value_stream_id=13`} + `( + 'when `valueStreamId` is `$valueStreamId`', + ({ isEditing, valueStreamId, text, cancelHref }) => { + beforeEach(() => { + createComponent({ props: { valueStreamId, isEditing } }); }); - }); - it('emits `clickPrimaryAction` event when primary action is selected', () => { - findPrimaryBtn().vm.$emit('click'); + it('renders primary action correctly', () => { + expect(findPrimaryBtn().text()).toBe(text); + expect(findPrimaryBtn().props()).toMatchObject({ + variant: 'confirm', + loading: false, + disabled: false, + }); + }); - expect(wrapper.emitted('clickPrimaryAction')).toHaveLength(1); - }); + it('emits `clickPrimaryAction` event when primary action is selected', () => { + findPrimaryBtn().vm.$emit('click'); - it('renders add stage action correctly', () => { - expect(findAddStageBtn().props()).toMatchObject({ - category: 'secondary', - variant: 'confirm', - disabled: false, + expect(wrapper.emitted('clickPrimaryAction')).toHaveLength(1); }); - }); - it('emits `clickAddStageAction` event when add stage action is selected', () => { - findAddStageBtn().vm.$emit('click'); - - expect(wrapper.emitted('clickAddStageAction')).toHaveLength(1); - }); + it('renders add stage action correctly', () => { + expect(findAddStageBtn().props()).toMatchObject({ + category: 'secondary', + variant: 'confirm', + disabled: false, + }); + }); - it('renders cancel button link correctly', () => { - expect(findValueStreamCancelBtn().props('disabled')).toBe(false); - expect(findValueStreamCancelBtn().attributes('href')).toBe(valueStreamPath); - }); + it('emits `clickAddStageAction` event when add stage action is selected', () => { + findAddStageBtn().vm.$emit('click'); - describe('isLoading=true', () => { - beforeEach(() => { - createComponent({ props: { isEditing, isLoading: true } }); + expect(wrapper.emitted('clickAddStageAction')).toHaveLength(1); }); - it('sets primary action to a loading state', () => { - expect(findPrimaryBtn().props('loading')).toBe(true); + it('renders cancel button link correctly', () => { + expect(findCancelBtn().props('disabled')).toBe(false); + expect(findCancelBtn().attributes('href')).toBe(cancelHref); }); - it('disables all other actions', () => { - expect(findAddStageBtn().props('disabled')).toBe(true); - expect(findValueStreamCancelBtn().props('disabled')).toBe(true); + describe('isLoading=true', () => { + beforeEach(() => { + createComponent({ props: { valueStreamId, isEditing, isLoading: true } }); + }); + + it('sets primary action to a loading state', () => { + expect(findPrimaryBtn().props('loading')).toBe(true); + }); + + it('disables all other actions', () => { + expect(findAddStageBtn().props('disabled')).toBe(true); + expect(findCancelBtn().props('disabled')).toBe(true); + }); }); - }); - }); + }, + ); }); diff --git a/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content_spec.js b/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content_spec.js index 79c5b6a3ca04b664872e4b44ef9facd3781bbf68..ac9bbc619bec58e0c5c85448f2e73c9df9b15efe 100644 --- a/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content_spec.js +++ b/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content_spec.js @@ -1,4 +1,4 @@ -import { GlAlert, GlFormInput } from '@gitlab/ui'; +import { GlAlert } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; // eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; @@ -8,7 +8,6 @@ import { PRESET_OPTIONS_DEFAULT, } from 'ee/analytics/cycle_analytics/vsa_settings/constants'; import CustomStageFields from 'ee/analytics/cycle_analytics/vsa_settings/components/custom_stage_fields.vue'; -import CustomStageEventField from 'ee/analytics/cycle_analytics/vsa_settings/components/custom_stage_event_field.vue'; import DefaultStageFields from 'ee/analytics/cycle_analytics/vsa_settings/components/default_stage_fields.vue'; import ValueStreamFormContent from 'ee/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content.vue'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; @@ -23,7 +22,6 @@ import { defaultStageConfig, rawCustomStage, groupLabels as defaultGroupLabels, - valueStreamPath, } from '../../mock_data'; jest.mock('~/lib/utils/url_utility', () => ({ @@ -37,19 +35,10 @@ describe('ValueStreamFormContent', () => { let wrapper = null; let trackingSpy = null; - const createValueStreamMock = jest.fn(() => Promise.resolve()); - const updateValueStreamMock = jest.fn(() => Promise.resolve()); - const mockToastShow = jest.fn(); + const mockValueStream = { id: 13 }; + const createValueStreamMock = jest.fn(() => Promise.resolve({ data: mockValueStream })); + const updateValueStreamMock = jest.fn(() => Promise.resolve({ data: mockValueStream })); const streamName = 'Cool stream'; - const initialFormNameErrors = { name: ['Name field required'] }; - const initialFormStageErrors = { - stages: [ - { - name: ['Name field is required'], - startEventIdentifier: ['Start event is required'], - }, - ], - }; const formSubmissionErrors = { name: ['has already been taken'], stages: [ @@ -65,16 +54,19 @@ describe('ValueStreamFormContent', () => { name: 'Editable value stream', }; - const initialPreset = PRESET_OPTIONS_DEFAULT; - - const fakeStore = ({ state }) => + const fakeStore = ({ state: stateOverrides }) => new Vuex.Store({ state: { - isCreatingValueStream: false, - isEditingValueStream: false, formEvents, defaultGroupLabels, - ...state, + createValueStreamErrors: {}, + selectedValueStream: undefined, + ...stateOverrides, + }, + mutations: { + setCreateValueStreamErrors(state, value) { + state.createValueStreamErrors = value; + }, }, actions: { createValueStream: createValueStreamMock, @@ -82,78 +74,55 @@ describe('ValueStreamFormContent', () => { }, }); - const createComponent = ({ props = {}, data = {}, stubs = {}, state = {} } = {}) => + const createComponent = ({ props = {}, state = {} } = {}) => shallowMountExtended(ValueStreamFormContent, { store: fakeStore({ state }), - data() { - return { - ...data, - }; - }, + provide: { vsaPath: '/mockPath' }, propsData: { defaultStageConfig, - valueStreamPath, ...props, }, - mocks: { - $toast: { - show: mockToastShow, - }, - }, - stubs: { - ...stubs, - }, }); const findFormActions = () => wrapper.findComponent(ValueStreamFormContentActions); - const findExtendedFormFields = () => wrapper.findByTestId('extended-form-fields'); - const findDefaultStages = () => findExtendedFormFields().findAllComponents(DefaultStageFields); - const findCustomStages = () => findExtendedFormFields().findAllComponents(CustomStageFields); + const findDefaultStages = () => wrapper.findAllComponents(DefaultStageFields); + const findCustomStages = () => wrapper.findAllComponents(CustomStageFields); const findLastCustomStage = () => findCustomStages().wrappers.at(-1); const findPresetSelector = () => wrapper.findByTestId('vsa-preset-selector'); const findRestoreButton = () => wrapper.findByTestId('vsa-reset-button'); const findRestoreStageButton = (index) => wrapper.findByTestId(`stage-action-restore-${index}`); const findHiddenStages = () => wrapper.findAllByTestId('vsa-hidden-stage').wrappers; - const findCustomStageEventField = (index = 0) => - wrapper.findAllComponents(CustomStageEventField).at(index); - const findFieldErrors = (testId) => wrapper.findByTestId(testId).attributes('invalid-feedback'); - const findNameInput = () => - wrapper.findByTestId('create-value-stream-name').findComponent(GlFormInput); + const findNameFormGroup = () => wrapper.findByTestId('create-value-stream-name'); + const findNameInput = () => wrapper.findByTestId('create-value-stream-name-input'); const findSubmitErrorAlert = () => wrapper.findComponent(GlAlert); - const fillStageNameAtIndex = (name, index) => - findCustomStages().at(index).findComponent(GlFormInput).vm.$emit('input', name); - const clickSubmit = () => findFormActions().vm.$emit('clickPrimaryAction'); const clickAddStage = async () => { findFormActions().vm.$emit('clickAddStageAction'); await nextTick(); }; const clickRestoreStageAtIndex = (index) => findRestoreStageButton(index).vm.$emit('click'); - const expectFieldError = (testId, error = '') => expect(findFieldErrors(testId)).toBe(error); - const expectCustomFieldError = (index, attr, error = '') => - expect(findCustomStageEventField(index).attributes(attr)).toBe(error); const expectStageTransitionKeys = (stages) => stages.forEach((stage) => expect(stage.transitionKey).toContain('stage-')); - describe('default state', () => { + const changeToDefaultStages = () => + findPresetSelector().vm.$emit('input', PRESET_OPTIONS_DEFAULT); + const changeToCustomStages = () => findPresetSelector().vm.$emit('input', PRESET_OPTIONS_BLANK); + + describe('when creating value stream', () => { beforeEach(() => { wrapper = createComponent({ state: { defaultGroupLabels: null } }); }); - it('has the form header', () => { + it('has the form actions', () => { expect(findFormActions().props()).toMatchObject({ isLoading: false, isEditing: false, - valueStreamPath, + valueStreamId: -1, }); }); - it('has the extended fields', () => { - expect(findExtendedFormFields().exists()).toBe(true); - }); - describe('Preset selector', () => { it('has the preset button', () => { expect(findPresetSelector().exists()).toBe(true); @@ -163,12 +132,12 @@ describe('ValueStreamFormContent', () => { expect(findDefaultStages()).toHaveLength(defaultStageConfig.length); expect(findCustomStages()).toHaveLength(0); - await findPresetSelector().vm.$emit('input', PRESET_OPTIONS_BLANK); + await changeToCustomStages(); expect(findDefaultStages()).toHaveLength(0); expect(findCustomStages()).toHaveLength(1); - await findPresetSelector().vm.$emit('input', PRESET_OPTIONS_DEFAULT); + await changeToDefaultStages(); expect(findDefaultStages()).toHaveLength(defaultStageConfig.length); expect(findCustomStages()).toHaveLength(0); @@ -179,21 +148,21 @@ describe('ValueStreamFormContent', () => { expect(findNameInput().attributes('value')).toBe(initialData.name); - await findPresetSelector().vm.$emit('input', PRESET_OPTIONS_BLANK); + await changeToCustomStages(); expect(findNameInput().attributes('value')).toBe(initialData.name); - await findPresetSelector().vm.$emit('input', PRESET_OPTIONS_DEFAULT); + await changeToDefaultStages(); expect(findNameInput().attributes('value')).toBe(initialData.name); }); it('each stage has a transition key when toggling', async () => { - await findPresetSelector().vm.$emit('input', PRESET_OPTIONS_BLANK); + await changeToCustomStages(); expectStageTransitionKeys(wrapper.vm.stages); - await findPresetSelector().vm.$emit('input', PRESET_OPTIONS_DEFAULT); + await changeToDefaultStages(); expectStageTransitionKeys(wrapper.vm.stages); }); @@ -205,11 +174,7 @@ describe('ValueStreamFormContent', () => { describe('Add stage button', () => { beforeEach(() => { - wrapper = createComponent({ - stubs: { - CustomStageFields, - }, - }); + wrapper = createComponent(); }); it('adds a blank custom stage when clicked', async () => { @@ -237,20 +202,15 @@ describe('ValueStreamFormContent', () => { describe('field validation', () => { beforeEach(() => { - wrapper = createComponent({ - stubs: { - CustomStageFields, - }, - }); + wrapper = createComponent(); }); it('validates existing fields when clicked', async () => { - const fieldTestId = 'create-value-stream-name'; - expect(findFieldErrors(fieldTestId)).toBeUndefined(); + expect(findNameFormGroup().attributes('invalid-feedback')).toBe(undefined); await clickAddStage(); - expectFieldError(fieldTestId, 'Name is required'); + expect(findNameFormGroup().attributes('invalid-feedback')).toBe('Name is required'); }); it('does not allow duplicate stage names', async () => { @@ -258,64 +218,56 @@ describe('ValueStreamFormContent', () => { await findNameInput().vm.$emit('input', streamName); await clickAddStage(); - await fillStageNameAtIndex(firstDefaultStage.name, 0); + await findCustomStages().at(0).vm.$emit('input', { + field: 'name', + value: firstDefaultStage.name, + }); // Trigger the field validation await clickAddStage(); - expectFieldError('custom-stage-name-3', 'Stage name already exists'); + expect(findCustomStages().at(0).props().errors.name).toEqual(['Stage name already exists']); }); }); describe('initial form stage errors', () => { - const commonExtendedData = { - props: { - initialFormErrors: initialFormStageErrors, - }, + const createValueStreamErrors = { + stages: [ + { + name: ['Name field is required'], + startEventIdentifier: ['Start event is required'], + }, + ], }; - it('renders errors for a default stage field', () => { + beforeEach(() => { wrapper = createComponent({ - ...commonExtendedData, - stubs: { - DefaultStageFields, - }, + state: { createValueStreamErrors }, }); - - expectFieldError('default-stage-name-0', initialFormStageErrors.stages[0].name[0]); }); - it('renders errors for a custom stage field', () => { - wrapper = createComponent({ - props: { - ...commonExtendedData.props, - initialPreset: PRESET_OPTIONS_BLANK, - }, - stubs: { - CustomStageFields, - }, - }); - - expectFieldError('custom-stage-name-0', initialFormStageErrors.stages[0].name[0]); - expectCustomFieldError( - 0, - 'identifiererror', - initialFormStageErrors.stages[0].startEventIdentifier[0], - ); + it('renders errors for a default stage field', () => { + expect(findDefaultStages().at(0).props().errors).toEqual(createValueStreamErrors.stages[0]); }); }); describe('initial form name errors', () => { + const nameError = 'Name field required'; + beforeEach(() => { wrapper = createComponent({ - props: { - initialFormErrors: initialFormNameErrors, + state: { + createValueStreamErrors: { name: [nameError] }, }, }); }); - it('renders errors for the name field', () => { - expectFieldError('create-value-stream-name', initialFormNameErrors.name[0]); + it('sets the feedback for the name form group', () => { + expect(findNameFormGroup().attributes('invalid-feedback')).toBe(nameError); + }); + + it('sets the state for the name input', () => { + expect(findNameInput().props().state).toBe(false); }); }); @@ -328,20 +280,6 @@ describe('ValueStreamFormContent', () => { unmockTracking(); }); - describe('form submitting', () => { - beforeEach(() => { - wrapper = createComponent({ - state: { - isCreatingValueStream: true, - }, - }); - }); - - it("enables form header's loading state", () => { - expect(findFormActions().props('isLoading')).toBe(true); - }); - }); - describe('form submitted successfully', () => { beforeEach(async () => { wrapper = createComponent(); @@ -370,10 +308,6 @@ describe('ValueStreamFormContent', () => { }); }); - it('does not display a toast message', () => { - expect(mockToastShow).not.toHaveBeenCalled(); - }); - it('sends tracking information', () => { expect(trackingSpy).toHaveBeenCalledWith(undefined, 'submit_form', { label: 'create_value_stream', @@ -385,7 +319,7 @@ describe('ValueStreamFormContent', () => { }); it('redirects to the new value stream page', () => { - expect(visitUrlWithAlerts).toHaveBeenCalledWith(valueStreamPath, [ + expect(visitUrlWithAlerts).toHaveBeenCalledWith('/mockPath?value_stream_id=13', [ { id: 'vsa-settings-form-submission-success', message: `'${streamName}' Value Stream has been successfully created.`, @@ -397,17 +331,12 @@ describe('ValueStreamFormContent', () => { describe('form submission fails', () => { beforeEach(async () => { - wrapper = createComponent({ - props: { - initialFormErrors: formSubmissionErrors, - }, - stubs: { - CustomStageFields, - }, - }); + wrapper = createComponent(); await findNameInput().vm.$emit('input', streamName); clickSubmit(); + + wrapper.vm.$store.commit('setCreateValueStreamErrors', formSubmissionErrors); }); it('calls the createValueStream action', () => { @@ -418,20 +347,18 @@ describe('ValueStreamFormContent', () => { expect(findNameInput().attributes('value')).toBe(streamName); }); - it('does not display a toast message', () => { - expect(mockToastShow).not.toHaveBeenCalled(); - }); - it('does not redirect to the new value stream page', () => { expect(visitUrlWithAlerts).not.toHaveBeenCalled(); }); - it('form header should not be in loading state', () => { + it('form actions should not be in loading state', () => { expect(findFormActions().props('isLoading')).toBe(false); }); it('renders errors for the name field', () => { - expectFieldError('create-value-stream-name', formSubmissionErrors.name[0]); + expect(findNameFormGroup().attributes('invalid-feedback')).toBe( + formSubmissionErrors.name[0], + ); }); it('renders a dismissible generic alert error', async () => { @@ -443,15 +370,17 @@ describe('ValueStreamFormContent', () => { }); }); - describe('isEditing=true', () => { + describe('when editing value stream', () => { const stageCount = initialData.stages.length; beforeEach(() => { wrapper = createComponent({ props: { - initialPreset, initialData, isEditing: true, }, + state: { + selectedValueStream: mockValueStream, + }, }); }); @@ -459,8 +388,12 @@ describe('ValueStreamFormContent', () => { expect(findPresetSelector().exists()).toBe(false); }); - it("enables form header's editing state", () => { - expect(findFormActions().props('isEditing')).toBe(true); + it('passes isEditing=true to form actions', () => { + expect(findFormActions().props().isEditing).toBe(true); + }); + + it('passes value stream ID to form actions', () => { + expect(findFormActions().props().valueStreamId).toBe(mockValueStream.id); }); it('does not display any hidden stages', () => { @@ -503,7 +436,6 @@ describe('ValueStreamFormContent', () => { beforeEach(() => { wrapper = createComponent({ props: { - initialPreset, initialData: { ...initialData, stages: [...initialData.stages, ...hiddenStages] }, isEditing: true, }, @@ -543,13 +475,9 @@ describe('ValueStreamFormContent', () => { beforeEach(() => { wrapper = createComponent({ props: { - initialPreset, initialData, isEditing: true, }, - stubs: { - CustomStageFields, - }, }); }); @@ -562,13 +490,12 @@ describe('ValueStreamFormContent', () => { }); it('validates existing fields when clicked', async () => { - const fieldTestId = 'create-value-stream-name'; - expect(findFieldErrors(fieldTestId)).toBeUndefined(); + expect(findNameInput().props().state).toBe(true); await findNameInput().vm.$emit('input', ''); await clickAddStage(); - expectFieldError(fieldTestId, 'Name is required'); + expect(findNameInput().props().state).toBe(false); }); }); @@ -581,32 +508,15 @@ describe('ValueStreamFormContent', () => { unmockTracking(); }); - describe('form submitting', () => { + describe('form submitted successfully', () => { beforeEach(() => { wrapper = createComponent({ props: { - initialPreset, initialData, isEditing: true, }, state: { - isEditingValueStream: true, - }, - }); - }); - - it("enables form header's loading state", () => { - expect(findFormActions().props('isLoading')).toBe(true); - }); - }); - - describe('form submitted successfully', () => { - beforeEach(() => { - wrapper = createComponent({ - props: { - initialPreset, - initialData, - isEditing: true, + selectedValueStream: mockValueStream, }, }); @@ -622,12 +532,12 @@ describe('ValueStreamFormContent', () => { }); }); - it('form header should be in loading state', () => { + it('form actions should be in loading state', () => { expect(findFormActions().props('isLoading')).toBe(true); }); it('redirects to the updated value stream page', () => { - expect(visitUrlWithAlerts).toHaveBeenCalledWith(valueStreamPath, [ + expect(visitUrlWithAlerts).toHaveBeenCalledWith('/mockPath?value_stream_id=13', [ { id: 'vsa-settings-form-submission-success', message: `'${initialData.name}' Value Stream has been successfully saved.`, @@ -647,17 +557,16 @@ describe('ValueStreamFormContent', () => { beforeEach(() => { wrapper = createComponent({ props: { - initialFormErrors: formSubmissionErrors, initialData, - initialPreset, isEditing: true, }, - stubs: { - CustomStageFields, + state: { + selectedValueStream: mockValueStream, }, }); clickSubmit(); + wrapper.vm.$store.commit('setCreateValueStreamErrors', formSubmissionErrors); }); it('calls the updateValueStreamMock action', () => { @@ -670,24 +579,24 @@ describe('ValueStreamFormContent', () => { expect(findNameInput().attributes('value')).toBe(name); }); - it('does not display a toast message', () => { - expect(mockToastShow).not.toHaveBeenCalled(); - }); - it('does not redirect to the value stream page', () => { expect(visitUrlWithAlerts).not.toHaveBeenCalled(); }); - it('form header should not be in loading state', () => { + it('form actions should not be in loading state', () => { expect(findFormActions().props('isLoading')).toBe(false); }); it('renders errors for the name field', () => { - expectFieldError('create-value-stream-name', formSubmissionErrors.name[0]); + expect(findNameFormGroup().attributes('invalid-feedback')).toBe( + formSubmissionErrors.name[0], + ); }); it('renders errors for a custom stage field', () => { - expectFieldError('custom-stage-name-0', formSubmissionErrors.stages[0].name[0]); + expect(findCustomStages().at(0).props().errors.name[0]).toBe( + formSubmissionErrors.stages[0].name[0], + ); }); it('renders a dismissible generic alert error', async () => { diff --git a/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/components/value_stream_form_spec.js b/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/components/value_stream_form_spec.js index a2472e6f1c76acbd152f438d82544fa11db424fd..dd55da879ac6f558f634f4c23128ba6c42a5f0e7 100644 --- a/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/components/value_stream_form_spec.js +++ b/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/components/value_stream_form_spec.js @@ -10,8 +10,6 @@ import { rawCustomStage, valueStreams, defaultStageConfig, - vsaPath, - valueStreamPath, groupLabels as defaultGroupLabels, } from 'ee_jest/analytics/cycle_analytics/mock_data'; @@ -30,7 +28,6 @@ describe('ValueStreamForm', () => { const fakeStore = ({ state }) => new Vuex.Store({ state: { - createValueStreamErrors: {}, defaultStageConfig, defaultGroupLabels, isLoading: false, @@ -50,9 +47,6 @@ describe('ValueStreamForm', () => { defaultStageConfig, ...props, }, - provide: { - vsaPath, - }, }); }; @@ -69,7 +63,6 @@ describe('ValueStreamForm', () => { defaultStageConfig, initialData, isEditing: false, - valueStreamPath: vsaPath, }); }); @@ -100,7 +93,6 @@ describe('ValueStreamForm', () => { defaultStageConfig, initialData: populatedInitialData, isEditing: true, - valueStreamPath, }); }); }); @@ -122,19 +114,6 @@ describe('ValueStreamForm', () => { }, ); - describe('with createValueStreamErrors', () => { - const nameError = "Name can't be blank"; - beforeEach(() => { - createComponent({ - state: { createValueStreamErrors: { name: nameError } }, - }); - }); - - it(`sets the form content component's initialFormErrors prop`, () => { - expect(findFormContent().props('initialFormErrors')).toEqual({ name: nameError }); - }); - }); - describe('when there are no defaultGroupLabels', () => { beforeEach(() => { createComponent({