Commit 2417c86b authored by Illya Klymov's avatar Illya Klymov

Merge branch '213732-form-errors-ux' into 'master'

Geo Form Validations

Closes #213732

See merge request gitlab-org/gitlab!32263
parents 64705d75 5ee882b4
<script> <script>
import { mapActions } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import { GlFormGroup, GlFormInput, GlFormCheckbox, GlDeprecatedButton } from '@gitlab/ui'; import { GlFormGroup, GlFormInput, GlFormCheckbox, GlButton } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import GeoNodeFormCore from './geo_node_form_core.vue'; import GeoNodeFormCore from './geo_node_form_core.vue';
...@@ -13,7 +13,7 @@ export default { ...@@ -13,7 +13,7 @@ export default {
GlFormGroup, GlFormGroup,
GlFormInput, GlFormInput,
GlFormCheckbox, GlFormCheckbox,
GlDeprecatedButton, GlButton,
GeoNodeFormCore, GeoNodeFormCore,
GeoNodeFormSelectiveSync, GeoNodeFormSelectiveSync,
GeoNodeFormCapacities, GeoNodeFormCapacities,
...@@ -53,6 +53,7 @@ export default { ...@@ -53,6 +53,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters(['formHasError']),
saveButtonTitle() { saveButtonTitle() {
return this.node ? __('Update') : __('Save'); return this.node ? __('Update') : __('Save');
}, },
...@@ -118,16 +119,17 @@ export default { ...@@ -118,16 +119,17 @@ export default {
</gl-form-group> </gl-form-group>
</section> </section>
<section class="d-flex align-items-center mt-4"> <section class="d-flex align-items-center mt-4">
<gl-deprecated-button <gl-button
id="node-save-button" id="node-save-button"
data-qa-selector="add_node_button" data-qa-selector="add_node_button"
variant="success" variant="success"
:disabled="formHasError"
@click="saveGeoNode(nodeData)" @click="saveGeoNode(nodeData)"
>{{ saveButtonTitle }}</gl-deprecated-button >{{ saveButtonTitle }}</gl-button
> >
<gl-deprecated-button id="node-cancel-button" class="ml-auto" @click="redirect">{{ <gl-button id="node-cancel-button" class="gl-ml-auto" @click="redirect">{{
__('Cancel') __('Cancel')
}}</gl-deprecated-button> }}</gl-button>
</section> </section>
</form> </form>
</template> </template>
<script> <script>
import { GlFormGroup, GlFormInput } from '@gitlab/ui'; import { GlFormGroup, GlFormInput } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { validateCapacity } from '../validations';
import { VALIDATION_FIELD_KEYS } from '../constants';
export default { export default {
name: 'GeoNodeFormCapacities', name: 'GeoNodeFormCapacities',
...@@ -23,7 +26,7 @@ export default { ...@@ -23,7 +26,7 @@ export default {
description: __( description: __(
'Control the maximum concurrency of repository backfill for this secondary node', 'Control the maximum concurrency of repository backfill for this secondary node',
), ),
key: 'reposMaxCapacity', key: VALIDATION_FIELD_KEYS.REPOS_MAX_CAPACITY,
conditional: 'secondary', conditional: 'secondary',
}, },
{ {
...@@ -32,7 +35,7 @@ export default { ...@@ -32,7 +35,7 @@ export default {
description: __( description: __(
'Control the maximum concurrency of LFS/attachment backfill for this secondary node', 'Control the maximum concurrency of LFS/attachment backfill for this secondary node',
), ),
key: 'filesMaxCapacity', key: VALIDATION_FIELD_KEYS.FILES_MAX_CAPACITY,
conditional: 'secondary', conditional: 'secondary',
}, },
{ {
...@@ -41,7 +44,7 @@ export default { ...@@ -41,7 +44,7 @@ export default {
description: __( description: __(
'Control the maximum concurrency of container repository operations for this Geo node', 'Control the maximum concurrency of container repository operations for this Geo node',
), ),
key: 'containerRepositoriesMaxCapacity', key: VALIDATION_FIELD_KEYS.CONTAINER_REPOSITORIES_MAX_CAPACITY,
conditional: 'secondary', conditional: 'secondary',
}, },
{ {
...@@ -50,7 +53,7 @@ export default { ...@@ -50,7 +53,7 @@ export default {
description: __( description: __(
'Control the maximum concurrency of verification operations for this Geo node', 'Control the maximum concurrency of verification operations for this Geo node',
), ),
key: 'verificationMaxCapacity', key: VALIDATION_FIELD_KEYS.VERIFICATION_MAX_CAPACITY,
}, },
{ {
id: 'node-reverification-interval-field', id: 'node-reverification-interval-field',
...@@ -58,13 +61,14 @@ export default { ...@@ -58,13 +61,14 @@ export default {
description: __( description: __(
'Control the minimum interval in days that a repository should be reverified for this primary node', 'Control the minimum interval in days that a repository should be reverified for this primary node',
), ),
key: 'minimumReverificationInterval', key: VALIDATION_FIELD_KEYS.MINIMUM_REVERIFICATION_INTERVAL,
conditional: 'primary', conditional: 'primary',
}, },
], ],
}; };
}, },
computed: { computed: {
...mapState(['formErrors']),
visibleFormGroups() { visibleFormGroups() {
return this.formGroups.filter(group => { return this.formGroups.filter(group => {
if (group.conditional) { if (group.conditional) {
...@@ -76,6 +80,15 @@ export default { ...@@ -76,6 +80,15 @@ export default {
}); });
}, },
}, },
methods: {
...mapActions(['setError']),
checkCapacity(formGroup) {
this.setError({
key: formGroup.key,
error: validateCapacity({ data: this.nodeData[formGroup.key], label: formGroup.label }),
});
},
},
}; };
</script> </script>
...@@ -87,12 +100,16 @@ export default { ...@@ -87,12 +100,16 @@ export default {
:label="formGroup.label" :label="formGroup.label"
:label-for="formGroup.id" :label-for="formGroup.id"
:description="formGroup.description" :description="formGroup.description"
:state="Boolean(formErrors[formGroup.key])"
:invalid-feedback="formErrors[formGroup.key]"
> >
<gl-form-input <gl-form-input
:id="formGroup.id" :id="formGroup.id"
v-model="nodeData[formGroup.key]" v-model="nodeData[formGroup.key]"
:class="{ 'is-invalid': Boolean(formErrors[formGroup.key]) }"
class="col-sm-3" class="col-sm-3"
type="number" type="number"
@input="checkCapacity(formGroup)"
/> />
</gl-form-group> </gl-form-group>
</div> </div>
......
<script> <script>
import { GlFormGroup, GlFormInput, GlSprintf } from '@gitlab/ui'; import { GlFormGroup, GlFormInput, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale'; import { mapActions, mapState } from 'vuex';
import { isSafeURL } from '~/lib/utils/url_utility'; import { validateName, validateUrl } from '../validations';
import { VALIDATION_FIELD_KEYS } from '../constants';
export default { export default {
name: 'GeoNodeFormCore', name: 'GeoNodeFormCore',
...@@ -16,29 +17,16 @@ export default { ...@@ -16,29 +17,16 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
fieldBlurs: {
name: false,
url: false,
},
errors: {
name: __('Name must be between 1 and 255 characters'),
url: __('URL must be a valid url (ex: https://gitlab.com)'),
},
};
},
computed: { computed: {
validName() { ...mapState(['formErrors']),
return !(this.fieldBlurs.name && (!this.nodeData.name || this.nodeData.name.length > 255));
},
validUrl() {
return !(this.fieldBlurs.url && !isSafeURL(this.nodeData.url));
},
}, },
methods: { methods: {
blur(field) { ...mapActions(['setError']),
this.fieldBlurs[field] = true; checkName() {
this.setError({ key: VALIDATION_FIELD_KEYS.NAME, error: validateName(this.nodeData.name) });
},
checkUrl() {
this.setError({ key: VALIDATION_FIELD_KEYS.URL, error: validateUrl(this.nodeData.url) });
}, },
}, },
}; };
...@@ -50,8 +38,8 @@ export default { ...@@ -50,8 +38,8 @@ export default {
class="col-sm-6" class="col-sm-6"
:label="__('Name')" :label="__('Name')"
label-for="node-name-field" label-for="node-name-field"
:state="validName" :state="Boolean(formErrors.name)"
:invalid-feedback="errors.name" :invalid-feedback="formErrors.name"
> >
<template #description> <template #description>
<gl-sprintf <gl-sprintf
...@@ -72,9 +60,10 @@ export default { ...@@ -72,9 +60,10 @@ export default {
<gl-form-input <gl-form-input
id="node-name-field" id="node-name-field"
v-model="nodeData.name" v-model="nodeData.name"
:class="{ 'is-invalid': Boolean(formErrors.name) }"
data-qa-selector="node_name_field" data-qa-selector="node_name_field"
type="text" type="text"
@blur="blur('name')" @input="checkName"
/> />
</gl-form-group> </gl-form-group>
<gl-form-group <gl-form-group
...@@ -82,15 +71,16 @@ export default { ...@@ -82,15 +71,16 @@ export default {
:label="__('URL')" :label="__('URL')"
label-for="node-url-field" label-for="node-url-field"
:description="__('The user-facing URL of the Geo node')" :description="__('The user-facing URL of the Geo node')"
:state="validUrl" :state="Boolean(formErrors.url)"
:invalid-feedback="errors.url" :invalid-feedback="formErrors.url"
> >
<gl-form-input <gl-form-input
id="node-url-field" id="node-url-field"
v-model="nodeData.url" v-model="nodeData.url"
:class="{ 'is-invalid': Boolean(formErrors.url) }"
data-qa-selector="node_url_field" data-qa-selector="node_url_field"
type="text" type="text"
@blur="blur('url')" @input="checkUrl"
/> />
</gl-form-group> </gl-form-group>
</section> </section>
......
export const SELECTIVE_SYNC_SHARDS = 'selectiveSyncShards'; export const SELECTIVE_SYNC_SHARDS = 'selectiveSyncShards';
export const SELECTIVE_SYNC_NAMESPACES = 'selectiveSyncNamespaceIds'; export const SELECTIVE_SYNC_NAMESPACES = 'selectiveSyncNamespaceIds';
export const VALIDATION_FIELD_KEYS = {
NAME: 'name',
URL: 'url',
REPOS_MAX_CAPACITY: 'reposMaxCapacity',
FILES_MAX_CAPACITY: 'filesMaxCapacity',
CONTAINER_REPOSITORIES_MAX_CAPACITY: 'containerRepositoriesMaxCapacity',
VERIFICATION_MAX_CAPACITY: 'verificationMaxCapacity',
MINIMUM_REVERIFICATION_INTERVAL: 'minimumReverificationInterval',
};
...@@ -64,3 +64,5 @@ export const saveGeoNode = ({ dispatch }, node) => { ...@@ -64,3 +64,5 @@ export const saveGeoNode = ({ dispatch }, node) => {
dispatch('receiveSaveGeoNodeError', response.data); dispatch('receiveSaveGeoNodeError', response.data);
}); });
}; };
export const setError = ({ commit }, { key, error }) => commit(types.SET_ERROR, { key, error });
// eslint-disable-next-line import/prefer-default-export
export const formHasError = state => Object.values(state.formErrors).some(val => Boolean(val));
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import * as actions from './actions'; import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
import createState from './state'; import createState from './state';
...@@ -10,6 +11,7 @@ const createStore = () => ...@@ -10,6 +11,7 @@ const createStore = () =>
new Vuex.Store({ new Vuex.Store({
actions, actions,
mutations, mutations,
getters,
state: createState(), state: createState(),
}); });
export default createStore; export default createStore;
...@@ -4,3 +4,5 @@ export const RECEIVE_SYNC_NAMESPACES_ERROR = 'RECEIVE_SYNC_NAMESPACES_ERROR'; ...@@ -4,3 +4,5 @@ export const RECEIVE_SYNC_NAMESPACES_ERROR = 'RECEIVE_SYNC_NAMESPACES_ERROR';
export const REQUEST_SAVE_GEO_NODE = 'REQUEST_SAVE_GEO_NODE'; export const REQUEST_SAVE_GEO_NODE = 'REQUEST_SAVE_GEO_NODE';
export const RECEIVE_SAVE_GEO_NODE_COMPLETE = 'RECEIVE_SAVE_GEO_NODE_COMPLETE'; export const RECEIVE_SAVE_GEO_NODE_COMPLETE = 'RECEIVE_SAVE_GEO_NODE_COMPLETE';
export const SET_ERROR = 'SET_ERROR';
...@@ -18,4 +18,7 @@ export default { ...@@ -18,4 +18,7 @@ export default {
[types.RECEIVE_SAVE_GEO_NODE_COMPLETE](state) { [types.RECEIVE_SAVE_GEO_NODE_COMPLETE](state) {
state.isLoading = false; state.isLoading = false;
}, },
[types.SET_ERROR](state, { key, error }) {
state.formErrors[key] = error;
},
}; };
import { VALIDATION_FIELD_KEYS } from '../constants';
const createState = () => ({ const createState = () => ({
isLoading: false, isLoading: false,
synchronizationNamespaces: [], synchronizationNamespaces: [],
formErrors: Object.values(VALIDATION_FIELD_KEYS).reduce(
(acc, cur) => ({ ...acc, [cur]: '' }),
{},
),
}); });
export default createState; export default createState;
import { sprintf, s__ } from '~/locale';
import { isSafeURL } from '~/lib/utils/url_utility';
export const validateName = data => {
if (!data) {
return s__("Geo|Node name can't be blank");
} else if (data.length > 255) {
return s__('Geo|Node name should be between 1 and 255 characters');
}
return '';
};
export const validateUrl = data => {
if (!data) {
return s__("Geo|URL can't be blank");
} else if (!isSafeURL(data)) {
return s__('Geo|URL must be a valid url (ex: https://gitlab.com)');
}
return '';
};
export const validateCapacity = ({ data, label }) => {
if (!data && data !== 0) {
return sprintf(s__("Geo|%{label} can't be blank"), { label });
} else if (data < 1 || data > 999) {
return sprintf(s__('Geo|%{label} should be between 1-999'), { label });
}
return '';
};
---
title: Geo Form Validations
merge_request: 32263
author:
type: changed
import { shallowMount } from '@vue/test-utils'; import Vuex from 'vuex';
import { createLocalVue, mount } from '@vue/test-utils';
import GeoNodeFormCapacities from 'ee/geo_node_form/components/geo_node_form_capacities.vue'; import GeoNodeFormCapacities from 'ee/geo_node_form/components/geo_node_form_capacities.vue';
import { VALIDATION_FIELD_KEYS } from 'ee/geo_node_form/constants';
import { MOCK_NODE } from '../mock_data'; import { MOCK_NODE } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('GeoNodeFormCapacities', () => { describe('GeoNodeFormCapacities', () => {
let wrapper; let wrapper;
let store;
const propsData = { const defaultProps = {
nodeData: MOCK_NODE, nodeData: MOCK_NODE,
}; };
const createComponent = () => { const createComponent = (props = {}) => {
wrapper = shallowMount(GeoNodeFormCapacities, { store = new Vuex.Store({
propsData, state: {
formErrors: Object.values(VALIDATION_FIELD_KEYS).reduce(
(acc, cur) => ({ ...acc, [cur]: '' }),
{},
),
},
actions: {
setError({ state }, { key, error }) {
state.formErrors[key] = error;
},
},
});
wrapper = mount(GeoNodeFormCapacities, {
localVue,
store,
propsData: {
...defaultProps,
...props,
},
}); });
}; };
...@@ -28,6 +53,7 @@ describe('GeoNodeFormCapacities', () => { ...@@ -28,6 +53,7 @@ describe('GeoNodeFormCapacities', () => {
wrapper.find('#node-verification-capacity-field'); wrapper.find('#node-verification-capacity-field');
const findGeoNodeFormReverificationIntervalField = () => const findGeoNodeFormReverificationIntervalField = () =>
wrapper.find('#node-reverification-interval-field'); wrapper.find('#node-reverification-interval-field');
const findErrorMessage = () => wrapper.find('.invalid-feedback');
describe('template', () => { describe('template', () => {
describe.each` describe.each`
...@@ -45,8 +71,9 @@ describe('GeoNodeFormCapacities', () => { ...@@ -45,8 +71,9 @@ describe('GeoNodeFormCapacities', () => {
showReverificationInterval, showReverificationInterval,
}) => { }) => {
beforeEach(() => { beforeEach(() => {
propsData.nodeData.primary = primaryNode; createComponent({
createComponent(); nodeData: { ...defaultProps.nodeData, primary: primaryNode },
});
}); });
it(`it ${showRepoCapacity ? 'shows' : 'hides'} the Repository Capacity Field`, () => { it(`it ${showRepoCapacity ? 'shows' : 'hides'} the Repository Capacity Field`, () => {
...@@ -82,14 +109,128 @@ describe('GeoNodeFormCapacities', () => { ...@@ -82,14 +109,128 @@ describe('GeoNodeFormCapacities', () => {
}); });
}, },
); );
describe.each`
data | showError | errorMessage
${null} | ${true} | ${"can't be blank"}
${''} | ${true} | ${"can't be blank"}
${-1} | ${true} | ${'should be between 1-999'}
${0} | ${true} | ${'should be between 1-999'}
${1} | ${false} | ${null}
${999} | ${false} | ${null}
${1000} | ${true} | ${'should be between 1-999'}
`(`errors`, ({ data, showError, errorMessage }) => {
describe('on primary node', () => {
beforeEach(() => {
createComponent({
nodeData: { ...defaultProps.nodeData, primary: true },
});
});
describe('Verification Capacity Field', () => {
beforeEach(() => {
findGeoNodeFormVerificationCapacityField().setValue(data);
});
it(`${showError ? 'shows' : 'hides'} error when data is ${data}`, () => {
expect(findGeoNodeFormVerificationCapacityField().classes('is-invalid')).toBe(
showError,
);
if (showError) {
expect(findErrorMessage().text()).toBe(`Verification capacity ${errorMessage}`);
}
});
});
describe('Reverification Interval Field', () => {
beforeEach(() => {
findGeoNodeFormReverificationIntervalField().setValue(data);
});
it(`${showError ? 'shows' : 'hides'} error when data is ${data}`, () => {
expect(findGeoNodeFormReverificationIntervalField().classes('is-invalid')).toBe(
showError,
);
if (showError) {
expect(findErrorMessage().text()).toBe(`Re-verification interval ${errorMessage}`);
}
});
});
});
describe('on secondary node', () => {
beforeEach(() => {
createComponent();
});
describe('Repository Capacity Field', () => {
beforeEach(() => {
findGeoNodeFormRepositoryCapacityField().setValue(data);
});
it(`${showError ? 'shows' : 'hides'} error when data is ${data}`, () => {
expect(findGeoNodeFormRepositoryCapacityField().classes('is-invalid')).toBe(showError);
if (showError) {
expect(findErrorMessage().text()).toBe(`Repository sync capacity ${errorMessage}`);
}
});
});
describe('File Capacity Field', () => {
beforeEach(() => {
findGeoNodeFormFileCapacityField().setValue(data);
});
it(`${showError ? 'shows' : 'hides'} error when data is ${data}`, () => {
expect(findGeoNodeFormFileCapacityField().classes('is-invalid')).toBe(showError);
if (showError) {
expect(findErrorMessage().text()).toBe(`File sync capacity ${errorMessage}`);
}
});
});
describe('Container Repository Capacity Field', () => {
beforeEach(() => {
findGeoNodeFormContainerRepositoryCapacityField().setValue(data);
});
it(`${showError ? 'shows' : 'hides'} error when data is ${data}`, () => {
expect(findGeoNodeFormContainerRepositoryCapacityField().classes('is-invalid')).toBe(
showError,
);
if (showError) {
expect(findErrorMessage().text()).toBe(
`Container repositories sync capacity ${errorMessage}`,
);
}
});
});
describe('Verification Capacity Field', () => {
beforeEach(() => {
findGeoNodeFormVerificationCapacityField().setValue(data);
});
it(`${showError ? 'shows' : 'hides'} error when data is ${data}`, () => {
expect(findGeoNodeFormVerificationCapacityField().classes('is-invalid')).toBe(
showError,
);
if (showError) {
expect(findErrorMessage().text()).toBe(`Verification capacity ${errorMessage}`);
}
});
});
});
});
}); });
describe('computed', () => { describe('computed', () => {
describe('visibleFormGroups', () => { describe('visibleFormGroups', () => {
describe('when nodeData.primary is true', () => { describe('when nodeData.primary is true', () => {
beforeEach(() => { beforeEach(() => {
propsData.nodeData.primary = true; createComponent({
createComponent(); nodeData: { ...defaultProps.nodeData, primary: true },
});
}); });
it('contains conditional form groups for primary', () => { it('contains conditional form groups for primary', () => {
...@@ -103,7 +244,6 @@ describe('GeoNodeFormCapacities', () => { ...@@ -103,7 +244,6 @@ describe('GeoNodeFormCapacities', () => {
describe('when nodeData.primary is false', () => { describe('when nodeData.primary is false', () => {
beforeEach(() => { beforeEach(() => {
propsData.nodeData.primary = false;
createComponent(); createComponent();
}); });
......
import { shallowMount } from '@vue/test-utils'; import Vuex from 'vuex';
import { createLocalVue, mount } from '@vue/test-utils';
import GeoNodeFormCore from 'ee/geo_node_form/components/geo_node_form_core.vue'; import GeoNodeFormCore from 'ee/geo_node_form/components/geo_node_form_core.vue';
import { VALIDATION_FIELD_KEYS } from 'ee/geo_node_form/constants';
import { MOCK_NODE, STRING_OVER_255 } from '../mock_data'; import { MOCK_NODE, STRING_OVER_255 } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('GeoNodeFormCore', () => { describe('GeoNodeFormCore', () => {
let wrapper; let wrapper;
let store;
const defaultProps = { const defaultProps = {
nodeData: MOCK_NODE, nodeData: MOCK_NODE,
}; };
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
wrapper = shallowMount(GeoNodeFormCore, { store = new Vuex.Store({
state: {
formErrors: Object.values(VALIDATION_FIELD_KEYS).reduce(
(acc, cur) => ({ ...acc, [cur]: '' }),
{},
),
},
actions: {
setError({ state }, { key, error }) {
state.formErrors[key] = error;
},
},
});
wrapper = mount(GeoNodeFormCore, {
localVue,
store,
propsData: { propsData: {
...defaultProps, ...defaultProps,
...props, ...props,
...@@ -24,6 +46,7 @@ describe('GeoNodeFormCore', () => { ...@@ -24,6 +46,7 @@ describe('GeoNodeFormCore', () => {
const findGeoNodeFormNameField = () => wrapper.find('#node-name-field'); const findGeoNodeFormNameField = () => wrapper.find('#node-name-field');
const findGeoNodeFormUrlField = () => wrapper.find('#node-url-field'); const findGeoNodeFormUrlField = () => wrapper.find('#node-url-field');
const findErrorMessage = () => wrapper.find('.invalid-feedback');
describe('template', () => { describe('template', () => {
beforeEach(() => { beforeEach(() => {
...@@ -37,68 +60,46 @@ describe('GeoNodeFormCore', () => { ...@@ -37,68 +60,46 @@ describe('GeoNodeFormCore', () => {
it('renders Geo Node Form Url Field', () => { it('renders Geo Node Form Url Field', () => {
expect(findGeoNodeFormUrlField().exists()).toBe(true); expect(findGeoNodeFormUrlField().exists()).toBe(true);
}); });
});
describe('computed', () => { describe('errors', () => {
describe.each` describe.each`
data | dataDesc | blur | value data | showError | errorMessage
${''} | ${'empty'} | ${false} | ${true} ${null} | ${true} | ${"Node name can't be blank"}
${''} | ${'empty'} | ${true} | ${false} ${''} | ${true} | ${"Node name can't be blank"}
${STRING_OVER_255} | ${'over 255 chars'} | ${false} | ${true} ${STRING_OVER_255} | ${true} | ${'Node name should be between 1 and 255 characters'}
${STRING_OVER_255} | ${'over 255 chars'} | ${true} | ${false} ${'Test'} | ${false} | ${null}
${'Test'} | ${'valid'} | ${false} | ${true} `(`Name Field`, ({ data, showError, errorMessage }) => {
${'Test'} | ${'valid'} | ${true} | ${true} beforeEach(() => {
`(`validName`, ({ data, dataDesc, blur, value }) => { createComponent();
beforeEach(() => { findGeoNodeFormNameField().setValue(data);
createComponent({
nodeData: { ...defaultProps.nodeData, name: data },
}); });
});
describe(`when data is: ${dataDesc}`, () => {
it(`returns ${value} when blur is ${blur}`, () => {
wrapper.vm.fieldBlurs.name = blur;
expect(wrapper.vm.validName).toBe(value); it(`${showError ? 'shows' : 'hides'} error when data is ${data}`, () => {
expect(findGeoNodeFormNameField().classes('is-invalid')).toBe(showError);
if (showError) {
expect(findErrorMessage().text()).toBe(errorMessage);
}
}); });
}); });
}); });
describe.each` describe.each`
data | dataDesc | blur | value data | showError | errorMessage
${''} | ${'empty'} | ${false} | ${true} ${null} | ${true} | ${"URL can't be blank"}
${''} | ${'empty'} | ${true} | ${false} ${''} | ${true} | ${"URL can't be blank"}
${'abcd'} | ${'invalid url'} | ${false} | ${true} ${'abcd'} | ${true} | ${'URL must be a valid url (ex: https://gitlab.com)'}
${'abcd'} | ${'invalid url'} | ${true} | ${false} ${'https://gitlab.com'} | ${false} | ${null}
${'https://gitlab.com'} | ${'valid url'} | ${false} | ${true} `(`Name Field`, ({ data, showError, errorMessage }) => {
${'https://gitlab.com'} | ${'valid url'} | ${true} | ${true}
`(`validUrl`, ({ data, dataDesc, blur, value }) => {
beforeEach(() => {
createComponent({
nodeData: { ...defaultProps.nodeData, url: data },
});
});
describe(`when data is: ${dataDesc}`, () => {
it(`returns ${value} when blur is ${blur}`, () => {
wrapper.vm.fieldBlurs.url = blur;
expect(wrapper.vm.validUrl).toBe(value);
});
});
});
});
describe('methods', () => {
describe('blur', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
findGeoNodeFormUrlField().setValue(data);
}); });
it('sets fieldBlur[field] to true', () => { it(`${showError ? 'shows' : 'hides'} error when data is ${data}`, () => {
expect(wrapper.vm.fieldBlurs.name).toBeFalsy(); expect(findGeoNodeFormUrlField().classes('is-invalid')).toBe(showError);
wrapper.vm.blur('name'); if (showError) {
expect(wrapper.vm.fieldBlurs.name).toBeTruthy(); expect(findErrorMessage().text()).toBe(errorMessage);
}
}); });
}); });
}); });
......
import { shallowMount } from '@vue/test-utils'; import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import GeoNodeForm from 'ee/geo_node_form/components/geo_node_form.vue'; import GeoNodeForm from 'ee/geo_node_form/components/geo_node_form.vue';
import GeoNodeFormCore from 'ee/geo_node_form/components/geo_node_form_core.vue'; import GeoNodeFormCore from 'ee/geo_node_form/components/geo_node_form_core.vue';
import GeoNodeFormCapacities from 'ee/geo_node_form/components/geo_node_form_capacities.vue'; import GeoNodeFormCapacities from 'ee/geo_node_form/components/geo_node_form_capacities.vue';
import store from 'ee/geo_node_form/store';
import { MOCK_NODE, MOCK_SELECTIVE_SYNC_TYPES, MOCK_SYNC_SHARDS } from '../mock_data'; import { MOCK_NODE, MOCK_SELECTIVE_SYNC_TYPES, MOCK_SYNC_SHARDS } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrlMock'), visitUrl: jest.fn().mockName('visitUrlMock'),
})); }));
...@@ -20,6 +25,7 @@ describe('GeoNodeForm', () => { ...@@ -20,6 +25,7 @@ describe('GeoNodeForm', () => {
const createComponent = () => { const createComponent = () => {
wrapper = shallowMount(GeoNodeForm, { wrapper = shallowMount(GeoNodeForm, {
store,
propsData, propsData,
}); });
}; };
...@@ -70,6 +76,24 @@ describe('GeoNodeForm', () => { ...@@ -70,6 +76,24 @@ describe('GeoNodeForm', () => {
}); });
}, },
); );
describe('Save Button', () => {
describe('with errors on form', () => {
beforeEach(() => {
wrapper.vm.$store.state.formErrors.name = 'Test Error';
});
it('disables button', () => {
expect(findGeoNodeSaveButton().attributes('disabled')).toBeTruthy();
});
});
describe('with mo errors on form', () => {
it('does not disable button', () => {
expect(findGeoNodeSaveButton().attributes('disabled')).toBeFalsy();
});
});
});
}); });
describe('methods', () => { describe('methods', () => {
......
...@@ -44,6 +44,7 @@ describe('GeoNodeForm Store Actions', () => { ...@@ -44,6 +44,7 @@ describe('GeoNodeForm Store Actions', () => {
${actions.requestSaveGeoNode} | ${null} | ${types.REQUEST_SAVE_GEO_NODE} | ${{ type: types.REQUEST_SAVE_GEO_NODE }} | ${noCallback} ${actions.requestSaveGeoNode} | ${null} | ${types.REQUEST_SAVE_GEO_NODE} | ${{ type: types.REQUEST_SAVE_GEO_NODE }} | ${noCallback}
${actions.receiveSaveGeoNodeSuccess} | ${null} | ${types.RECEIVE_SAVE_GEO_NODE_COMPLETE} | ${{ type: types.RECEIVE_SAVE_GEO_NODE_COMPLETE }} | ${visitUrlCallback} ${actions.receiveSaveGeoNodeSuccess} | ${null} | ${types.RECEIVE_SAVE_GEO_NODE_COMPLETE} | ${{ type: types.RECEIVE_SAVE_GEO_NODE_COMPLETE }} | ${visitUrlCallback}
${actions.receiveSaveGeoNodeError} | ${{ message: MOCK_ERROR_MESSAGE }} | ${types.RECEIVE_SAVE_GEO_NODE_COMPLETE} | ${{ type: types.RECEIVE_SAVE_GEO_NODE_COMPLETE }} | ${flashCallback} ${actions.receiveSaveGeoNodeError} | ${{ message: MOCK_ERROR_MESSAGE }} | ${types.RECEIVE_SAVE_GEO_NODE_COMPLETE} | ${{ type: types.RECEIVE_SAVE_GEO_NODE_COMPLETE }} | ${flashCallback}
${actions.setError} | ${{ key: 'name', error: 'error' }} | ${types.SET_ERROR} | ${{ type: types.SET_ERROR, payload: { key: 'name', error: 'error' } }} | ${noCallback}
`(`non-axios calls`, ({ action, data, mutationName, mutationCall, callback }) => { `(`non-axios calls`, ({ action, data, mutationName, mutationCall, callback }) => {
describe(action.name, () => { describe(action.name, () => {
it(`should commit mutation ${mutationName}`, () => { it(`should commit mutation ${mutationName}`, () => {
......
import * as getters from 'ee/geo_node_form/store/getters';
import createState from 'ee/geo_node_form/store/state';
describe('GeoNodeForm Store Getters', () => {
let state;
beforeEach(() => {
state = createState();
});
describe('formHasError', () => {
it('with error returns true', () => {
state.formErrors.name = 'Error';
expect(getters.formHasError(state)).toBeTruthy();
});
it('without error returns false', () => {
expect(getters.formHasError(state)).toBeFalsy();
});
});
});
...@@ -43,4 +43,11 @@ describe('GeoNodeForm Store Mutations', () => { ...@@ -43,4 +43,11 @@ describe('GeoNodeForm Store Mutations', () => {
expect(state.synchronizationNamespaces).toEqual([]); expect(state.synchronizationNamespaces).toEqual([]);
}); });
}); });
describe('SET_ERROR', () => {
it('sets error for field', () => {
mutations[types.SET_ERROR](state, { key: 'name', error: 'error' });
expect(state.formErrors.name).toBe('error');
});
});
}); });
import { validateName, validateUrl, validateCapacity } from 'ee/geo_node_form/validations';
import { STRING_OVER_255 } from './mock_data';
describe('GeoNodeForm Validations', () => {
describe.each`
data | errorMessage
${null} | ${"Node name can't be blank"}
${''} | ${"Node name can't be blank"}
${STRING_OVER_255} | ${'Node name should be between 1 and 255 characters'}
${'Test'} | ${''}
`(`validateName`, ({ data, errorMessage }) => {
let validateNameRes = '';
beforeEach(() => {
validateNameRes = validateName(data);
});
it(`return ${errorMessage} when data is ${data}`, () => {
expect(validateNameRes).toBe(errorMessage);
});
});
describe.each`
data | errorMessage
${null} | ${"URL can't be blank"}
${''} | ${"URL can't be blank"}
${'abcd'} | ${'URL must be a valid url (ex: https://gitlab.com)'}
${'https://gitlab.com'} | ${''}
`(`validateUrl`, ({ data, errorMessage }) => {
let validateUrlRes = '';
beforeEach(() => {
validateUrlRes = validateUrl(data);
});
it(`return ${errorMessage} when data is ${data}`, () => {
expect(validateUrlRes).toBe(errorMessage);
});
});
describe.each`
data | errorMessage
${null} | ${"Mock field can't be blank"}
${''} | ${"Mock field can't be blank"}
${-1} | ${'Mock field should be between 1-999'}
${0} | ${'Mock field should be between 1-999'}
${1} | ${''}
${999} | ${''}
${1000} | ${'Mock field should be between 1-999'}
`(`validateCapacity`, ({ data, errorMessage }) => {
let validateCapacityRes = '';
beforeEach(() => {
validateCapacityRes = validateCapacity({ data, label: 'Mock field' });
});
it(`return ${errorMessage} when data is ${data}`, () => {
expect(validateCapacityRes).toBe(errorMessage);
});
});
});
...@@ -10065,6 +10065,12 @@ msgstr "" ...@@ -10065,6 +10065,12 @@ msgstr ""
msgid "GeoNodes|secondary nodes" msgid "GeoNodes|secondary nodes"
msgstr "" msgstr ""
msgid "Geo|%{label} can't be blank"
msgstr ""
msgid "Geo|%{label} should be between 1-999"
msgstr ""
msgid "Geo|%{name} is scheduled for forced re-download" msgid "Geo|%{name} is scheduled for forced re-download"
msgstr "" msgstr ""
...@@ -10128,6 +10134,12 @@ msgstr "" ...@@ -10128,6 +10134,12 @@ msgstr ""
msgid "Geo|Next sync scheduled at" msgid "Geo|Next sync scheduled at"
msgstr "" msgstr ""
msgid "Geo|Node name can't be blank"
msgstr ""
msgid "Geo|Node name should be between 1 and 255 characters"
msgstr ""
msgid "Geo|Not synced yet" msgid "Geo|Not synced yet"
msgstr "" msgstr ""
...@@ -10212,6 +10224,12 @@ msgstr "" ...@@ -10212,6 +10224,12 @@ msgstr ""
msgid "Geo|Tracking entry for upload (%{type}/%{id}) was successfully removed." msgid "Geo|Tracking entry for upload (%{type}/%{id}) was successfully removed."
msgstr "" msgstr ""
msgid "Geo|URL can't be blank"
msgstr ""
msgid "Geo|URL must be a valid url (ex: https://gitlab.com)"
msgstr ""
msgid "Geo|Unknown state" msgid "Geo|Unknown state"
msgstr "" msgstr ""
...@@ -14047,9 +14065,6 @@ msgstr "" ...@@ -14047,9 +14065,6 @@ msgstr ""
msgid "Name has already been taken" msgid "Name has already been taken"
msgstr "" msgstr ""
msgid "Name must be between 1 and 255 characters"
msgstr ""
msgid "Name new label" msgid "Name new label"
msgstr "" msgstr ""
...@@ -23107,9 +23122,6 @@ msgstr "" ...@@ -23107,9 +23122,6 @@ msgstr ""
msgid "URL is required" msgid "URL is required"
msgstr "" msgstr ""
msgid "URL must be a valid url (ex: https://gitlab.com)"
msgstr ""
msgid "URL must start with %{codeStart}http://%{codeEnd}, %{codeStart}https://%{codeEnd}, or %{codeStart}ftp://%{codeEnd}" msgid "URL must start with %{codeStart}http://%{codeEnd}, %{codeStart}https://%{codeEnd}, or %{codeStart}ftp://%{codeEnd}"
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