Commit 5ee882b4 authored by Zack Cuddy's avatar Zack Cuddy

Geo Form Validations

Currently the Geo Form does not respond
to form errors very intuitively.

This adds validations to the form for
name, url, and all the capcities.

This also disables the Save button
when an error is present.
parent f6697c96
<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}
`(`validName`, ({ data, dataDesc, blur, value }) => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent();
nodeData: { ...defaultProps.nodeData, name: data }, findGeoNodeFormNameField().setValue(data);
});
}); });
describe(`when data is: ${dataDesc}`, () => { it(`${showError ? 'shows' : 'hides'} error when data is ${data}`, () => {
it(`returns ${value} when blur is ${blur}`, () => { expect(findGeoNodeFormNameField().classes('is-invalid')).toBe(showError);
wrapper.vm.fieldBlurs.name = blur; if (showError) {
expect(findErrorMessage().text()).toBe(errorMessage);
expect(wrapper.vm.validName).toBe(value); }
}); });
}); });
}); });
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);
});
});
});
...@@ -10031,6 +10031,12 @@ msgstr "" ...@@ -10031,6 +10031,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 ""
...@@ -10094,6 +10100,12 @@ msgstr "" ...@@ -10094,6 +10100,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 ""
...@@ -10178,6 +10190,12 @@ msgstr "" ...@@ -10178,6 +10190,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 ""
...@@ -14004,9 +14022,6 @@ msgstr "" ...@@ -14004,9 +14022,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 ""
...@@ -23022,9 +23037,6 @@ msgstr "" ...@@ -23022,9 +23037,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