Commit 6fcbcda2 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo Committed by Brandon Labuschagne

Add value stream form specs

Separates the jest test for the value
stream selector and the value stream form
parent e733d1f4
<script>
import { GlForm, GlFormInput, GlFormGroup, GlModal } from '@gitlab/ui';
import { debounce } from 'lodash';
import { mapState, mapActions } from 'vuex';
import { sprintf, __ } from '~/locale';
import { DATA_REFETCH_DELAY } from '../../shared/constants';
const ERRORS = {
MIN_LENGTH: __('Name is required'),
MAX_LENGTH: __('Maximum length 100 characters'),
};
const NAME_MAX_LENGTH = 100;
const validate = ({ name }) => {
const errors = { name: [] };
if (name.length > NAME_MAX_LENGTH) {
errors.name.push(ERRORS.MAX_LENGTH);
}
if (!name.length) {
errors.name.push(ERRORS.MIN_LENGTH);
}
return errors;
};
const I18N = {
CREATE_VALUE_STREAM: __('Create Value Stream'),
CREATED: __("'%{name}' Value Stream created"),
CANCEL: __('Cancel'),
MODAL_TITLE: __('Value Stream Name'),
FIELD_NAME_LABEL: __('Name'),
FIELD_NAME_PLACEHOLDER: __('Example: My Value Stream'),
};
export default {
name: 'ValueStreamForm',
components: {
GlForm,
GlFormInput,
GlFormGroup,
GlModal,
},
props: {
initialData: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
return {
errors: {},
name: '',
...this.initialData,
};
},
computed: {
...mapState({
initialFormErrors: 'createValueStreamErrors',
isCreating: 'isCreatingValueStream',
}),
isValid() {
return !this.errors.name?.length;
},
invalidFeedback() {
return this.errors.name?.join('\n');
},
hasFormErrors() {
const { initialFormErrors } = this;
return Boolean(Object.keys(initialFormErrors).length);
},
isLoading() {
return this.isCreating;
},
primaryProps() {
return {
text: this.$options.I18N.CREATE_VALUE_STREAM,
attributes: [
{ variant: 'success' },
{ disabled: !this.isValid },
{ loading: this.isLoading },
],
};
},
},
watch: {
initialFormErrors(newErrors = {}) {
this.errors = newErrors;
},
},
mounted() {
const { initialFormErrors } = this;
if (this.hasFormErrors) {
this.errors = initialFormErrors;
} else {
this.onHandleInput();
}
},
methods: {
...mapActions(['createValueStream']),
onHandleInput: debounce(function debouncedValidation() {
const { name } = this;
this.errors = validate({ name });
}, DATA_REFETCH_DELAY),
onSubmit() {
const { name } = this;
return this.createValueStream({ name }).then(() => {
if (!this.hasFormErrors) {
this.$toast.show(sprintf(this.$options.I18N.CREATED, { name }), {
position: 'top-center',
});
this.name = '';
}
});
},
},
I18N,
};
</script>
<template>
<gl-modal
data-testid="value-stream-form-modal"
modal-id="value-stream-form-modal"
:title="$options.I18N.MODAL_TITLE"
:action-primary="primaryProps"
:action-cancel="{ text: $options.I18N.CANCEL }"
@primary.prevent="onSubmit"
>
<gl-form>
<gl-form-group
:label="$options.I18N.FIELD_NAME_LABEL"
label-for="create-value-stream-name"
:invalid-feedback="invalidFeedback"
:state="isValid"
>
<gl-form-input
id="create-value-stream-name"
v-model.trim="name"
name="create-value-stream-name"
:placeholder="$options.I18N.FIELD_NAME_PLACEHOLDER"
:state="isValid"
required
@input="onHandleInput"
/>
</gl-form-group>
</gl-form>
</gl-modal>
</template>
......@@ -5,31 +5,20 @@ import {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlForm,
GlFormInput,
GlFormGroup,
GlModal,
GlModalDirective,
} from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { debounce } from 'lodash';
import { sprintf, __ } from '~/locale';
import { DATA_REFETCH_DELAY } from '../../shared/constants';
import ValueStreamForm from './value_stream_form.vue';
const ERRORS = {
MIN_LENGTH: __('Name is required'),
MAX_LENGTH: __('Maximum length 100 characters'),
};
const validate = ({ name }) => {
const errors = { name: [] };
if (name.length > 100) {
errors.name.push(ERRORS.MAX_LENGTH);
}
if (!name.length) {
errors.name.push(ERRORS.MIN_LENGTH);
}
return errors;
const I18N = {
DELETE_NAME: __('Delete %{name}'),
DELETE_CONFIRMATION: __('Are you sure you want to delete "%{name}" Value Stream?'),
DELETED: __("'%{name}' Value Stream deleted"),
DELETE: __('Delete'),
CREATE_VALUE_STREAM: __('Create new Value Stream'),
CANCEL: __('Cancel'),
};
export default {
......@@ -39,38 +28,19 @@ export default {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlForm,
GlFormInput,
GlFormGroup,
GlModal,
ValueStreamForm,
},
directives: {
GlModalDirective,
},
data() {
return {
name: '',
errors: {},
};
},
computed: {
...mapState({
isDeleting: 'isDeletingValueStream',
isCreating: 'isCreatingValueStream',
deleteValueStreamError: 'deleteValueStreamError',
initialFormErrors: 'createValueStreamErrors',
data: 'valueStreams',
selectedValueStream: 'selectedValueStream',
}),
isLoading() {
return this.isDeleting || this.isCreating;
},
isValid() {
return !this.errors.name?.length;
},
invalidFeedback() {
return this.errors.name?.join('\n');
},
hasValueStreams() {
return Boolean(this.data.length);
},
......@@ -83,49 +53,20 @@ export default {
canDeleteSelectedStage() {
return this.selectedValueStream?.isCustom || false;
},
hasFormErrors() {
const { initialFormErrors } = this;
return Boolean(Object.keys(initialFormErrors).length);
},
deleteSelectedText() {
return sprintf(__('Delete %{name}'), { name: this.selectedValueStreamName });
return sprintf(this.$options.I18N.DELETE_NAME, { name: this.selectedValueStreamName });
},
deleteConfirmationText() {
return sprintf(__('Are you sure you want to delete "%{name}" Value Stream?'), {
return sprintf(this.$options.I18N.DELETE_CONFIRMATION, {
name: this.selectedValueStreamName,
});
},
},
watch: {
initialFormErrors(newErrors = {}) {
this.errors = newErrors;
},
},
mounted() {
const { initialFormErrors } = this;
if (this.hasFormErrors) {
this.errors = initialFormErrors;
} else {
this.onHandleInput();
}
},
methods: {
...mapActions(['createValueStream', 'setSelectedValueStream', 'deleteValueStream']),
onSubmit() {
const { name } = this;
return this.createValueStream({ name }).then(() => {
if (!this.hasFormErrors) {
this.$toast.show(sprintf(__("'%{name}' Value Stream created"), { name }), {
position: 'top-center',
});
this.name = '';
}
});
...mapActions(['setSelectedValueStream', 'deleteValueStream']),
onSuccess(message) {
this.$toast.show(message, { position: 'top-center' });
},
onHandleInput: debounce(function debouncedValidation() {
const { name } = this;
this.errors = validate({ name });
}, DATA_REFETCH_DELAY),
isSelected(id) {
return Boolean(this.selectedValueStreamId && this.selectedValueStreamId === id);
},
......@@ -136,17 +77,16 @@ export default {
const name = this.selectedValueStreamName;
return this.deleteValueStream(this.selectedValueStreamId).then(() => {
if (!this.deleteValueStreamError) {
this.$toast.show(sprintf(__("'%{name}' Value Stream deleted"), { name }), {
position: 'top-center',
});
this.onSuccess(sprintf(this.$options.I18N.DELETED, { name }));
}
});
},
},
I18N,
};
</script>
<template>
<gl-form>
<div>
<gl-dropdown
v-if="hasValueStreams"
data-testid="dropdown-value-streams"
......@@ -162,8 +102,8 @@ export default {
>{{ streamName }}</gl-dropdown-item
>
<gl-dropdown-divider />
<gl-dropdown-item v-gl-modal-directive="'create-value-stream-modal'" @click="onHandleInput">{{
__('Create new Value Stream')
<gl-dropdown-item v-gl-modal-directive="'value-stream-form-modal'">{{
$options.I18N.CREATE_VALUE_STREAM
}}</gl-dropdown-item>
<gl-dropdown-item
v-if="canDeleteSelectedStage"
......@@ -173,52 +113,19 @@ export default {
>{{ deleteSelectedText }}</gl-dropdown-item
>
</gl-dropdown>
<gl-button v-else v-gl-modal-directive="'create-value-stream-modal'" @click="onHandleInput">{{
__('Create new Value Stream')
<gl-button v-else v-gl-modal-directive="'value-stream-form-modal'">{{
$options.I18N.CREATE_VALUE_STREAM
}}</gl-button>
<gl-modal
data-testid="create-value-stream-modal"
modal-id="create-value-stream-modal"
:title="__('Value Stream Name')"
:action-primary="{
text: __('Create Value Stream'),
attributes: [
{ variant: 'success' },
{
disabled: !isValid,
},
{ loading: isLoading },
],
}"
:action-cancel="{ text: __('Cancel') }"
@primary.prevent="onSubmit"
>
<gl-form-group
:label="__('Name')"
label-for="create-value-stream-name"
:invalid-feedback="invalidFeedback"
:state="isValid"
>
<gl-form-input
id="create-value-stream-name"
v-model.trim="name"
name="create-value-stream-name"
:placeholder="__('Example: My Value Stream')"
:state="isValid"
required
@input="onHandleInput"
/>
</gl-form-group>
</gl-modal>
<value-stream-form />
<gl-modal
data-testid="delete-value-stream-modal"
modal-id="delete-value-stream-modal"
:title="__('Delete Value Stream')"
:action-primary="{
text: __('Delete'),
attributes: [{ variant: 'danger' }, { loading: isLoading }],
text: $options.I18N.DELETE,
attributes: [{ variant: 'danger' }, { loading: isDeleting }],
}"
:action-cancel="{ text: __('Cancel') }"
:action-cancel="{ text: $options.I18N.CANCEL }"
@primary.prevent="onDelete"
>
<gl-alert v-if="deleteValueStreamError" variant="danger">{{
......@@ -226,5 +133,5 @@ export default {
}}</gl-alert>
<p>{{ deleteConfirmationText }}</p>
</gl-modal>
</gl-form>
</div>
</template>
import { GlModal, GlFormGroup } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import ValueStreamForm from 'ee/analytics/cycle_analytics/components/value_stream_form.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('ValueStreamForm', () => {
let wrapper = null;
const createValueStreamMock = jest.fn(() => Promise.resolve());
const mockEvent = { preventDefault: jest.fn() };
const mockToastShow = jest.fn();
const streamName = 'Cool stream';
const createValueStreamErrors = { name: ['Name field required'] };
const fakeStore = ({ initialState = {} }) =>
new Vuex.Store({
state: {
isCreatingValueStream: false,
createValueStreamErrors: {},
...initialState,
},
actions: {
createValueStream: createValueStreamMock,
},
});
const createComponent = ({ data = {}, initialState = {} } = {}) =>
shallowMount(ValueStreamForm, {
localVue,
store: fakeStore({ initialState }),
data() {
return {
...data,
};
},
mocks: {
$toast: {
show: mockToastShow,
},
},
});
const findModal = () => wrapper.find(GlModal);
const createSubmitButtonDisabledState = () =>
findModal().props('actionPrimary').attributes[1].disabled;
const submitModal = () => findModal().vm.$emit('primary', mockEvent);
const findFormGroup = () => wrapper.find(GlFormGroup);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('default state', () => {
beforeEach(() => {
wrapper = createComponent({ initialState: {} });
});
it('submit button is disabled', () => {
expect(createSubmitButtonDisabledState()).toBe(true);
});
});
describe('form errors', () => {
beforeEach(() => {
wrapper = createComponent({
data: { name: streamName },
initialState: {
createValueStreamErrors,
},
});
});
it('renders the error', () => {
expect(findFormGroup().attributes('invalid-feedback')).toEqual(
createValueStreamErrors.name.join('\n'),
);
});
it('submit button is disabled', () => {
expect(createSubmitButtonDisabledState()).toBe(true);
});
});
describe('with valid fields', () => {
beforeEach(() => {
wrapper = createComponent({ data: { name: streamName } });
});
it('submit button is enabled', () => {
expect(createSubmitButtonDisabledState()).toBe(false);
});
describe('form submitted successfully', () => {
beforeEach(() => {
submitModal();
});
it('calls the "createValueStream" event when submitted', () => {
expect(createValueStreamMock).toHaveBeenCalledWith(expect.any(Object), {
name: streamName,
});
});
it('clears the name field', () => {
expect(wrapper.vm.name).toBe('');
});
it('displays a toast message', () => {
expect(mockToastShow).toHaveBeenCalledWith(`'${streamName}' Value Stream created`, {
position: 'top-center',
});
});
});
describe('form submission fails', () => {
beforeEach(() => {
wrapper = createComponent({
data: { name: streamName },
initialState: {
createValueStreamErrors,
},
});
submitModal();
});
it('calls the createValueStream action', () => {
expect(createValueStreamMock).toHaveBeenCalled();
});
it('does not clear the name field', () => {
expect(wrapper.vm.name).toBe(streamName);
});
it('does not display a toast message', () => {
expect(mockToastShow).not.toHaveBeenCalled();
});
});
});
});
import { GlButton, GlDropdown, GlFormGroup } from '@gitlab/ui';
import { GlButton, GlDropdown } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import ValueStreamSelect from 'ee/analytics/cycle_analytics/components/value_stream_select.vue';
......@@ -17,7 +17,6 @@ describe('ValueStreamSelect', () => {
const mockToastShow = jest.fn();
const streamName = 'Cool stream';
const selectedValueStream = valueStreams[0];
const createValueStreamErrors = { name: ['Name field required'] };
const deleteValueStreamError = 'Cannot delete default value stream';
const fakeStore = ({ initialState = {} }) =>
......@@ -54,14 +53,11 @@ describe('ValueStreamSelect', () => {
});
const findModal = modal => wrapper.find(`[data-testid="${modal}-value-stream-modal"]`);
const createSubmitButtonDisabledState = () =>
findModal('create').props('actionPrimary').attributes[1].disabled;
const submitModal = modal => findModal(modal).vm.$emit('primary', mockEvent);
const findSelectValueStreamDropdown = () => wrapper.find(GlDropdown);
const findSelectValueStreamDropdownOptions = _wrapper => findDropdownItemText(_wrapper);
const findCreateValueStreamButton = () => wrapper.find(GlButton);
const findDeleteValueStreamButton = () => wrapper.find('[data-testid="delete-value-stream"]');
const findFormGroup = () => wrapper.find(GlFormGroup);
beforeEach(() => {
wrapper = createComponent({
......@@ -156,90 +152,6 @@ describe('ValueStreamSelect', () => {
});
});
describe('Create value stream form', () => {
it('submit button is disabled', () => {
expect(createSubmitButtonDisabledState()).toBe(true);
});
describe('form errors', () => {
beforeEach(() => {
wrapper = createComponent({
data: { name: streamName },
initialState: {
createValueStreamErrors,
},
});
});
it('renders the error', () => {
expect(findFormGroup().attributes('invalid-feedback')).toEqual(
createValueStreamErrors.name.join('\n'),
);
});
it('submit button is disabled', () => {
expect(createSubmitButtonDisabledState()).toBe(true);
});
});
describe('with valid fields', () => {
beforeEach(() => {
wrapper = createComponent({ data: { name: streamName } });
});
it('submit button is enabled', () => {
expect(createSubmitButtonDisabledState()).toBe(false);
});
describe('form submitted successfully', () => {
beforeEach(() => {
submitModal('create');
});
it('calls the "createValueStream" event when submitted', () => {
expect(createValueStreamMock).toHaveBeenCalledWith(expect.any(Object), {
name: streamName,
});
});
it('clears the name field', () => {
expect(wrapper.vm.name).toEqual('');
});
it('displays a toast message', () => {
expect(mockToastShow).toHaveBeenCalledWith(`'${streamName}' Value Stream created`, {
position: 'top-center',
});
});
});
describe('form submission fails', () => {
beforeEach(() => {
wrapper = createComponent({
data: { name: streamName },
initialState: {
createValueStreamErrors,
},
});
submitModal('create');
});
it('calls the createValueStream action', () => {
expect(createValueStreamMock).toHaveBeenCalled();
});
it('does not clear the name field', () => {
expect(wrapper.vm.name).toEqual(streamName);
});
it('does not display a toast message', () => {
expect(mockToastShow).not.toHaveBeenCalled();
});
});
});
});
describe('Delete value stream modal', () => {
describe('succeeds', () => {
beforeEach(() => {
......
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