Commit c090e3f5 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '37242-refactor-expiration-policy-form' into 'master'

Refactor docker container expiration policy form

See merge request gitlab-org/gitlab!24276
parents 4201c84d a95e5c54
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { GlCard, GlButton, GlLoadingIcon } from '@gitlab/ui';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { import {
UPDATE_SETTINGS_ERROR_MESSAGE, UPDATE_SETTINGS_ERROR_MESSAGE,
UPDATE_SETTINGS_SUCCESS_MESSAGE, UPDATE_SETTINGS_SUCCESS_MESSAGE,
} from '../../shared/constants'; } from '../../shared/constants';
import { mapComputed } from '~/vuex_shared/bindings'; import { mapComputed } from '~/vuex_shared/bindings';
import ExpirationPolicyForm from '../../shared/components/expiration_policy_form.vue'; import ExpirationPolicyFields from '../../shared/components/expiration_policy_fields.vue';
export default { export default {
components: { components: {
ExpirationPolicyForm, GlCard,
GlButton,
GlLoadingIcon,
ExpirationPolicyFields,
}, },
mixins: [Tracking.mixin()], mixins: [Tracking.mixin()],
labelsConfig: { labelsConfig: {
...@@ -22,12 +26,19 @@ export default { ...@@ -22,12 +26,19 @@ export default {
tracking: { tracking: {
label: 'docker_container_retention_and_expiration_policies', label: 'docker_container_retention_and_expiration_policies',
}, },
formIsValid: true,
}; };
}, },
computed: { computed: {
...mapState(['formOptions', 'isLoading']), ...mapState(['formOptions', 'isLoading']),
...mapGetters({ isEdited: 'getIsEdited' }), ...mapGetters({ isEdited: 'getIsEdited' }),
...mapComputed([{ key: 'settings', getter: 'getSettings' }], 'updateSettings'), ...mapComputed([{ key: 'settings', getter: 'getSettings' }], 'updateSettings'),
isSubmitButtonDisabled() {
return !this.formIsValid || this.isLoading;
},
isCancelButtonDisabled() {
return !this.isEdited || this.isLoading;
},
}, },
methods: { methods: {
...mapActions(['resetSettings', 'saveSettings']), ...mapActions(['resetSettings', 'saveSettings']),
...@@ -46,12 +57,42 @@ export default { ...@@ -46,12 +57,42 @@ export default {
</script> </script>
<template> <template>
<expiration-policy-form <form ref="form-element" @submit.prevent="submit" @reset.prevent="reset">
v-model="settings" <gl-card>
:form-options="formOptions" <template #header>
:is-loading="isLoading" {{ s__('ContainerRegistry|Tag expiration policy') }}
:disable-cancel-button="!isEdited" </template>
@submit="submit" <template #default>
@reset="reset" <expiration-policy-fields
/> v-model="settings"
:form-options="formOptions"
:is-loading="isLoading"
@validated="formIsValid = true"
@invalidated="formIsValid = false"
/>
</template>
<template #footer>
<div class="d-flex justify-content-end">
<gl-button
ref="cancel-button"
type="reset"
class="mr-2 d-block"
:disabled="isCancelButtonDisabled"
>
{{ __('Cancel') }}
</gl-button>
<gl-button
ref="save-button"
type="submit"
:disabled="isSubmitButtonDisabled"
variant="success"
class="d-flex justify-content-center align-items-center js-no-auto-disable"
>
{{ __('Save expiration policy') }}
<gl-loading-icon v-if="isLoading" class="ml-2" />
</gl-button>
</div>
</template>
</gl-card>
</form>
</template> </template>
<script>
import { uniqueId } from 'lodash';
import { GlFormGroup, GlToggle, GlFormSelect, GlFormTextarea } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
import { NAME_REGEX_LENGTH } from '../constants';
import { mapComputedToEvent } from '../utils';
export default {
components: {
GlFormGroup,
GlToggle,
GlFormSelect,
GlFormTextarea,
},
props: {
formOptions: {
type: Object,
required: false,
default: () => ({}),
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
value: {
type: Object,
required: false,
default: () => ({}),
},
labelCols: {
type: [Number, String],
required: false,
default: 3,
},
labelAlign: {
type: String,
required: false,
default: 'right',
},
},
nameRegexPlaceholder: '.*',
selectList: [
{
name: 'expiration-policy-interval',
label: s__('ContainerRegistry|Expiration interval:'),
model: 'older_than',
optionKey: 'olderThan',
},
{
name: 'expiration-policy-schedule',
label: s__('ContainerRegistry|Expiration schedule:'),
model: 'cadence',
optionKey: 'cadence',
},
{
name: 'expiration-policy-latest',
label: s__('ContainerRegistry|Number of tags to retain:'),
model: 'keep_n',
optionKey: 'keepN',
},
],
data() {
return {
uniqueId: uniqueId(),
};
},
computed: {
...mapComputedToEvent(['enabled', 'cadence', 'older_than', 'keep_n', 'name_regex'], 'value'),
policyEnabledText() {
return this.enabled ? __('enabled') : __('disabled');
},
toggleDescriptionText() {
return sprintf(
s__('ContainerRegistry|Docker tag expiration policy is %{toggleStatus}'),
{
toggleStatus: `<strong>${this.policyEnabledText}</strong>`,
},
false,
);
},
regexHelpText() {
return sprintf(
s__(
'ContainerRegistry|Wildcards such as %{codeStart}*-stable%{codeEnd} or %{codeStart}production/*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}',
),
{
codeStart: '<code>',
codeEnd: '</code>',
},
false,
);
},
nameRegexState() {
return this.name_regex ? this.name_regex.length <= NAME_REGEX_LENGTH : null;
},
fieldsValidity() {
return this.nameRegexState !== false;
},
isFormElementDisabled() {
return !this.enabled || this.isLoading;
},
},
watch: {
fieldsValidity: {
immediate: true,
handler(valid) {
if (valid) {
this.$emit('validated');
} else {
this.$emit('invalidated');
}
},
},
},
methods: {
idGenerator(id) {
return `${id}_${this.uniqueId}`;
},
updateModel(value, key) {
this[key] = value;
},
},
};
</script>
<template>
<div ref="form-elements" class="lh-2">
<gl-form-group
:id="idGenerator('expiration-policy-toggle-group')"
:label-cols="labelCols"
:label-align="labelAlign"
:label-for="idGenerator('expiration-policy-toggle')"
:label="s__('ContainerRegistry|Expiration policy:')"
>
<div class="d-flex align-items-start">
<gl-toggle
:id="idGenerator('expiration-policy-toggle')"
v-model="enabled"
:disabled="isLoading"
/>
<span class="mb-2 ml-1 lh-2" v-html="toggleDescriptionText"></span>
</div>
</gl-form-group>
<gl-form-group
v-for="select in $options.selectList"
:id="idGenerator(`${select.name}-group`)"
:key="select.name"
:label-cols="labelCols"
:label-align="labelAlign"
:label-for="idGenerator(select.name)"
:label="select.label"
>
<gl-form-select
:id="idGenerator(select.name)"
:value="value[select.model]"
:disabled="isFormElementDisabled"
@input="updateModel($event, select.model)"
>
<option
v-for="option in formOptions[select.optionKey]"
:key="option.key"
:value="option.key"
>
{{ option.label }}
</option>
</gl-form-select>
</gl-form-group>
<gl-form-group
:id="idGenerator('expiration-policy-name-matching-group')"
:label-cols="labelCols"
:label-align="labelAlign"
:label-for="idGenerator('expiration-policy-name-matching')"
:label="
s__('ContainerRegistry|Docker tags with names matching this regex pattern will expire:')
"
:state="nameRegexState"
:invalid-feedback="
s__('ContainerRegistry|The value of this input should be less than 255 characters')
"
>
<gl-form-textarea
:id="idGenerator('expiration-policy-name-matching')"
v-model="name_regex"
:placeholder="$options.nameRegexPlaceholder"
:state="nameRegexState"
:disabled="isFormElementDisabled"
trim
/>
<template #description>
<span ref="regex-description" v-html="regexHelpText"></span>
</template>
</gl-form-group>
</div>
</template>
<script>
import { uniqueId } from 'lodash';
import {
GlFormGroup,
GlToggle,
GlFormSelect,
GlFormTextarea,
GlButton,
GlCard,
GlLoadingIcon,
} from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
import { NAME_REGEX_LENGTH } from '../constants';
import { mapComputedToEvent } from '../utils';
export default {
components: {
GlFormGroup,
GlToggle,
GlFormSelect,
GlFormTextarea,
GlButton,
GlCard,
GlLoadingIcon,
},
props: {
formOptions: {
type: Object,
required: false,
default: () => ({}),
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
value: {
type: Object,
required: false,
default: () => ({}),
},
labelCols: {
type: [Number, String],
required: false,
default: 3,
},
labelAlign: {
type: String,
required: false,
default: 'right',
},
disableCancelButton: {
type: Boolean,
required: false,
default: false,
},
},
nameRegexPlaceholder: '.*',
data() {
return {
uniqueId: uniqueId(),
};
},
computed: {
...mapComputedToEvent(['enabled', 'cadence', 'older_than', 'keep_n', 'name_regex'], 'value'),
policyEnabledText() {
return this.enabled ? __('enabled') : __('disabled');
},
toggleDescriptionText() {
return sprintf(
s__('ContainerRegistry|Docker tag expiration policy is %{toggleStatus}'),
{
toggleStatus: `<strong>${this.policyEnabledText}</strong>`,
},
false,
);
},
regexHelpText() {
return sprintf(
s__(
'ContainerRegistry|Wildcards such as %{codeStart}*-stable%{codeEnd} or %{codeStart}production/*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}',
),
{
codeStart: '<code>',
codeEnd: '</code>',
},
false,
);
},
nameRegexState() {
return this.name_regex ? this.name_regex.length <= NAME_REGEX_LENGTH : null;
},
formIsInvalid() {
return this.nameRegexState === false;
},
isFormElementDisabled() {
return !this.enabled || this.isLoading;
},
isSubmitButtonDisabled() {
return this.formIsInvalid || this.isLoading;
},
isCancelButtonDisabled() {
return this.disableCancelButton || this.isLoading;
},
},
methods: {
idGenerator(id) {
return `${id}_${this.uniqueId}`;
},
},
};
</script>
<template>
<form
ref="form-element"
class="lh-2"
@submit.prevent="$emit('submit')"
@reset.prevent="$emit('reset')"
>
<gl-card>
<template #header>
{{ s__('ContainerRegistry|Tag expiration policy') }}
</template>
<template>
<gl-form-group
:id="idGenerator('expiration-policy-toggle-group')"
:label-cols="labelCols"
:label-align="labelAlign"
:label-for="idGenerator('expiration-policy-toggle')"
:label="s__('ContainerRegistry|Expiration policy:')"
>
<div class="d-flex align-items-start">
<gl-toggle
:id="idGenerator('expiration-policy-toggle')"
v-model="enabled"
:disabled="isLoading"
/>
<span class="mb-2 ml-1 lh-2" v-html="toggleDescriptionText"></span>
</div>
</gl-form-group>
<gl-form-group
:id="idGenerator('expiration-policy-interval-group')"
:label-cols="labelCols"
:label-align="labelAlign"
:label-for="idGenerator('expiration-policy-interval')"
:label="s__('ContainerRegistry|Expiration interval:')"
>
<gl-form-select
:id="idGenerator('expiration-policy-interval')"
v-model="older_than"
:disabled="isFormElementDisabled"
>
<option v-for="option in formOptions.olderThan" :key="option.key" :value="option.key">
{{ option.label }}
</option>
</gl-form-select>
</gl-form-group>
<gl-form-group
:id="idGenerator('expiration-policy-schedule-group')"
:label-cols="labelCols"
:label-align="labelAlign"
:label-for="idGenerator('expiration-policy-schedule')"
:label="s__('ContainerRegistry|Expiration schedule:')"
>
<gl-form-select
:id="idGenerator('expiration-policy-schedule')"
v-model="cadence"
:disabled="isFormElementDisabled"
>
<option v-for="option in formOptions.cadence" :key="option.key" :value="option.key">
{{ option.label }}
</option>
</gl-form-select>
</gl-form-group>
<gl-form-group
:id="idGenerator('expiration-policy-latest-group')"
:label-cols="labelCols"
:label-align="labelAlign"
:label-for="idGenerator('expiration-policy-latest')"
:label="s__('ContainerRegistry|Number of tags to retain:')"
>
<gl-form-select
:id="idGenerator('expiration-policy-latest')"
v-model="keep_n"
:disabled="isFormElementDisabled"
>
<option v-for="option in formOptions.keepN" :key="option.key" :value="option.key">
{{ option.label }}
</option>
</gl-form-select>
</gl-form-group>
<gl-form-group
:id="idGenerator('expiration-policy-name-matching-group')"
:label-cols="labelCols"
:label-align="labelAlign"
:label-for="idGenerator('expiration-policy-name-matching')"
:label="
s__('ContainerRegistry|Docker tags with names matching this regex pattern will expire:')
"
:state="nameRegexState"
:invalid-feedback="
s__('ContainerRegistry|The value of this input should be less than 255 characters')
"
>
<gl-form-textarea
:id="idGenerator('expiration-policy-name-matching')"
v-model="name_regex"
:placeholder="$options.nameRegexPlaceholder"
:state="nameRegexState"
:disabled="isFormElementDisabled"
trim
/>
<template #description>
<span ref="regex-description" v-html="regexHelpText"></span>
</template>
</gl-form-group>
</template>
<template #footer>
<div class="d-flex justify-content-end">
<gl-button
ref="cancel-button"
type="reset"
class="mr-2 d-block"
:disabled="isCancelButtonDisabled"
>
{{ __('Cancel') }}
</gl-button>
<gl-button
ref="save-button"
type="submit"
:disabled="isSubmitButtonDisabled"
variant="success"
class="d-flex justify-content-center align-items-center js-no-auto-disable"
>
{{ __('Save expiration policy') }}
<gl-loading-icon v-if="isLoading" class="ml-2" />
</gl-button>
</div>
</template>
</gl-card>
</form>
</template>
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import component from '~/registry/settings/components/settings_form.vue'; import component from '~/registry/settings/components/settings_form.vue';
import expirationPolicyForm from '~/registry/shared/components/expiration_policy_form.vue'; import expirationPolicyFields from '~/registry/shared/components/expiration_policy_fields.vue';
import { createStore } from '~/registry/settings/store/'; import { createStore } from '~/registry/settings/store/';
import { import {
UPDATE_SETTINGS_ERROR_MESSAGE, UPDATE_SETTINGS_ERROR_MESSAGE,
...@@ -14,14 +14,34 @@ describe('Settings Form', () => { ...@@ -14,14 +14,34 @@ describe('Settings Form', () => {
let store; let store;
let dispatchSpy; let dispatchSpy;
const GlLoadingIcon = { name: 'gl-loading-icon-stub', template: '<svg></svg>' };
const GlCard = {
name: 'gl-card-stub',
template: `
<div>
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
</div>
`,
};
const trackingPayload = { const trackingPayload = {
label: 'docker_container_retention_and_expiration_policies', label: 'docker_container_retention_and_expiration_policies',
}; };
const findForm = () => wrapper.find(expirationPolicyForm); const findForm = () => wrapper.find({ ref: 'form-element' });
const findFields = () => wrapper.find(expirationPolicyFields);
const findCancelButton = () => wrapper.find({ ref: 'cancel-button' });
const findSaveButton = () => wrapper.find({ ref: 'save-button' });
const findLoadingIcon = (parent = wrapper) => parent.find(GlLoadingIcon);
const mountComponent = () => { const mountComponent = () => {
wrapper = shallowMount(component, { wrapper = shallowMount(component, {
stubs: {
GlCard,
GlLoadingIcon,
},
mocks: { mocks: {
$toast: { $toast: {
show: jest.fn(), show: jest.fn(),
...@@ -47,46 +67,50 @@ describe('Settings Form', () => { ...@@ -47,46 +67,50 @@ describe('Settings Form', () => {
let form; let form;
beforeEach(() => { beforeEach(() => {
form = findForm(); form = findForm();
dispatchSpy.mockReturnValue();
}); });
describe('data binding', () => { describe('data binding', () => {
it('v-model change update the settings property', () => { it('v-model change update the settings property', () => {
dispatchSpy.mockReturnValue(); findFields().vm.$emit('input', 'foo');
form.vm.$emit('input', 'foo');
expect(dispatchSpy).toHaveBeenCalledWith('updateSettings', { settings: 'foo' }); expect(dispatchSpy).toHaveBeenCalledWith('updateSettings', { settings: 'foo' });
}); });
}); });
describe('form reset event', () => { describe('form reset event', () => {
beforeEach(() => {
form.trigger('reset');
});
it('calls the appropriate function', () => { it('calls the appropriate function', () => {
dispatchSpy.mockReturnValue();
form.vm.$emit('reset');
expect(dispatchSpy).toHaveBeenCalledWith('resetSettings'); expect(dispatchSpy).toHaveBeenCalledWith('resetSettings');
}); });
it('tracks the reset event', () => { it('tracks the reset event', () => {
dispatchSpy.mockReturnValue();
form.vm.$emit('reset');
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'reset_form', trackingPayload); expect(Tracking.event).toHaveBeenCalledWith(undefined, 'reset_form', trackingPayload);
}); });
}); });
describe('form submit event ', () => { describe('form submit event ', () => {
it('save has type submit', () => {
mountComponent();
expect(findSaveButton().attributes('type')).toBe('submit');
});
it('dispatches the saveSettings action', () => { it('dispatches the saveSettings action', () => {
dispatchSpy.mockResolvedValue(); dispatchSpy.mockResolvedValue();
form.vm.$emit('submit'); form.trigger('submit');
expect(dispatchSpy).toHaveBeenCalledWith('saveSettings'); expect(dispatchSpy).toHaveBeenCalledWith('saveSettings');
}); });
it('tracks the submit event', () => { it('tracks the submit event', () => {
dispatchSpy.mockResolvedValue(); dispatchSpy.mockResolvedValue();
form.vm.$emit('submit'); form.trigger('submit');
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'submit_form', trackingPayload); expect(Tracking.event).toHaveBeenCalledWith(undefined, 'submit_form', trackingPayload);
}); });
it('show a success toast when submit succeed', () => { it('show a success toast when submit succeed', () => {
dispatchSpy.mockResolvedValue(); dispatchSpy.mockResolvedValue();
form.vm.$emit('submit'); form.trigger('submit');
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE, { expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE, {
type: 'success', type: 'success',
...@@ -96,7 +120,7 @@ describe('Settings Form', () => { ...@@ -96,7 +120,7 @@ describe('Settings Form', () => {
it('show an error toast when submit fails', () => { it('show an error toast when submit fails', () => {
dispatchSpy.mockRejectedValue(); dispatchSpy.mockRejectedValue();
form.vm.$emit('submit'); form.trigger('submit');
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE, { expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE, {
type: 'error', type: 'error',
...@@ -105,4 +129,52 @@ describe('Settings Form', () => { ...@@ -105,4 +129,52 @@ describe('Settings Form', () => {
}); });
}); });
}); });
describe('form actions', () => {
describe('cancel button', () => {
beforeEach(() => {
store.commit('SET_SETTINGS', { foo: 'bar' });
});
it('has type reset', () => {
expect(findCancelButton().attributes('type')).toBe('reset');
});
it('is disabled when isEdited is false', () =>
wrapper.vm.$nextTick().then(() => {
expect(findCancelButton().attributes('disabled')).toBe('true');
}));
it('is disabled isLoading is true', () => {
store.commit('TOGGLE_LOADING');
store.commit('UPDATE_SETTINGS', { settings: { foo: 'baz' } });
return wrapper.vm.$nextTick().then(() => {
expect(findCancelButton().attributes('disabled')).toBe('true');
store.commit('TOGGLE_LOADING');
});
});
it('is enabled when isLoading is false and isEdited is true', () => {
store.commit('UPDATE_SETTINGS', { settings: { foo: 'baz' } });
return wrapper.vm.$nextTick().then(() => {
expect(findCancelButton().attributes('disabled')).toBe(undefined);
});
});
});
describe('when isLoading is true', () => {
beforeEach(() => {
store.commit('TOGGLE_LOADING');
});
afterEach(() => {
store.commit('TOGGLE_LOADING');
});
it('submit button is disabled and shows a spinner', () => {
const button = findSaveButton();
expect(button.attributes('disabled')).toBeTruthy();
expect(findLoadingIcon(button).exists()).toBe(true);
});
});
});
}); });
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Expiration Policy Form renders 1`] = `
<div
class="lh-2"
>
<glformgroup-stub
id="expiration-policy-toggle-group"
label="Expiration policy:"
label-align="right"
label-cols="3"
label-for="expiration-policy-toggle"
>
<div
class="d-flex align-items-start"
>
<gltoggle-stub
id="expiration-policy-toggle"
labeloff="Toggle Status: OFF"
labelon="Toggle Status: ON"
/>
<span
class="mb-2 ml-1 lh-2"
>
Docker tag expiration policy is
<strong>
disabled
</strong>
</span>
</div>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-interval-group"
label="Expiration interval:"
label-align="right"
label-cols="3"
label-for="expiration-policy-interval"
>
<glformselect-stub
disabled="true"
id="expiration-policy-interval"
>
<option
value="foo"
>
Foo
</option>
<option
value="bar"
>
Bar
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-schedule-group"
label="Expiration schedule:"
label-align="right"
label-cols="3"
label-for="expiration-policy-schedule"
>
<glformselect-stub
disabled="true"
id="expiration-policy-schedule"
>
<option
value="foo"
>
Foo
</option>
<option
value="bar"
>
Bar
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-latest-group"
label="Number of tags to retain:"
label-align="right"
label-cols="3"
label-for="expiration-policy-latest"
>
<glformselect-stub
disabled="true"
id="expiration-policy-latest"
>
<option
value="foo"
>
Foo
</option>
<option
value="bar"
>
Bar
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-name-matching-group"
invalid-feedback="The value of this input should be less than 255 characters"
label="Docker tags with names matching this regex pattern will expire:"
label-align="right"
label-cols="3"
label-for="expiration-policy-name-matching"
>
<glformtextarea-stub
disabled="true"
id="expiration-policy-name-matching"
placeholder=".*"
trim=""
value=""
/>
</glformgroup-stub>
</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Expiration Policy Form renders 1`] = `
<form
class="lh-2"
>
<div
class="card"
>
<!---->
<div
class="card-header"
>
Tag expiration policy
</div>
<div
class="card-body"
>
<!---->
<!---->
<glformgroup-stub
id="expiration-policy-toggle-group"
label="Expiration policy:"
label-align="right"
label-cols="3"
label-for="expiration-policy-toggle"
>
<div
class="d-flex align-items-start"
>
<gltoggle-stub
id="expiration-policy-toggle"
labeloff="Toggle Status: OFF"
labelon="Toggle Status: ON"
/>
<span
class="mb-2 ml-1 lh-2"
>
Docker tag expiration policy is
<strong>
disabled
</strong>
</span>
</div>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-interval-group"
label="Expiration interval:"
label-align="right"
label-cols="3"
label-for="expiration-policy-interval"
>
<glformselect-stub
disabled="true"
id="expiration-policy-interval"
>
<option
value="foo"
>
Foo
</option>
<option
value="bar"
>
Bar
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-schedule-group"
label="Expiration schedule:"
label-align="right"
label-cols="3"
label-for="expiration-policy-schedule"
>
<glformselect-stub
disabled="true"
id="expiration-policy-schedule"
>
<option
value="foo"
>
Foo
</option>
<option
value="bar"
>
Bar
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-latest-group"
label="Number of tags to retain:"
label-align="right"
label-cols="3"
label-for="expiration-policy-latest"
>
<glformselect-stub
disabled="true"
id="expiration-policy-latest"
>
<option
value="foo"
>
Foo
</option>
<option
value="bar"
>
Bar
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-name-matching-group"
invalid-feedback="The value of this input should be less than 255 characters"
label="Docker tags with names matching this regex pattern will expire:"
label-align="right"
label-cols="3"
label-for="expiration-policy-name-matching"
>
<glformtextarea-stub
disabled="true"
id="expiration-policy-name-matching"
placeholder=".*"
trim=""
value=""
/>
</glformgroup-stub>
</div>
<div
class="card-footer"
>
<div
class="d-flex justify-content-end"
>
<glbutton-stub
class="mr-2 d-block"
size="md"
type="reset"
variant="secondary"
>
Cancel
</glbutton-stub>
<glbutton-stub
class="d-flex justify-content-center align-items-center js-no-auto-disable"
size="md"
type="submit"
variant="success"
>
Save expiration policy
<!---->
</glbutton-stub>
</div>
</div>
<!---->
</div>
</form>
`;
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import stubChildren from 'helpers/stub_children'; import stubChildren from 'helpers/stub_children';
import component from '~/registry/shared/components/expiration_policy_form.vue'; import component from '~/registry/shared/components/expiration_policy_fields.vue';
import { NAME_REGEX_LENGTH } from '~/registry/shared/constants'; import { NAME_REGEX_LENGTH } from '~/registry/shared/constants';
import { formOptions } from '../mock_data'; import { formOptions } from '../mock_data';
...@@ -10,22 +10,14 @@ describe('Expiration Policy Form', () => { ...@@ -10,22 +10,14 @@ describe('Expiration Policy Form', () => {
const FORM_ELEMENTS_ID_PREFIX = '#expiration-policy'; const FORM_ELEMENTS_ID_PREFIX = '#expiration-policy';
const GlLoadingIcon = { name: 'gl-loading-icon-stub', template: '<svg></svg>' };
const findFormGroup = name => wrapper.find(`${FORM_ELEMENTS_ID_PREFIX}-${name}-group`); const findFormGroup = name => wrapper.find(`${FORM_ELEMENTS_ID_PREFIX}-${name}-group`);
const findFormElements = (name, parent = wrapper) => const findFormElements = (name, parent = wrapper) =>
parent.find(`${FORM_ELEMENTS_ID_PREFIX}-${name}`); parent.find(`${FORM_ELEMENTS_ID_PREFIX}-${name}`);
const findCancelButton = () => wrapper.find({ ref: 'cancel-button' });
const findSaveButton = () => wrapper.find({ ref: 'save-button' });
const findForm = () => wrapper.find({ ref: 'form-element' });
const findLoadingIcon = (parent = wrapper) => parent.find(GlLoadingIcon);
const mountComponent = props => { const mountComponent = props => {
wrapper = mount(component, { wrapper = mount(component, {
stubs: { stubs: {
...stubChildren(component), ...stubChildren(component),
GlCard: false,
GlLoadingIcon,
}, },
propsData: { propsData: {
formOptions, formOptions,
...@@ -114,77 +106,20 @@ describe('Expiration Policy Form', () => { ...@@ -114,77 +106,20 @@ describe('Expiration Policy Form', () => {
}, },
); );
describe('form actions', () => { describe('when isLoading is true', () => {
describe('cancel button', () => { beforeEach(() => {
it('has type reset', () => { mountComponent({ isLoading: true });
mountComponent();
expect(findCancelButton().attributes('type')).toBe('reset');
});
it('is disabled when disableCancelButton is true', () => {
mountComponent({ disableCancelButton: true });
return wrapper.vm.$nextTick().then(() => {
expect(findCancelButton().attributes('disabled')).toBe('true');
});
});
it('is disabled isLoading is true', () => {
mountComponent({ isLoading: true });
return wrapper.vm.$nextTick().then(() => {
expect(findCancelButton().attributes('disabled')).toBe('true');
});
});
it('is enabled when isLoading and disableCancelButton are false', () => {
mountComponent({ disableCancelButton: false, isLoading: false });
return wrapper.vm.$nextTick().then(() => {
expect(findCancelButton().attributes('disabled')).toBe(undefined);
});
});
});
describe('form cancel event', () => {
it('calls the appropriate function', () => {
mountComponent();
findForm().trigger('reset');
expect(wrapper.emitted('reset')).toBeTruthy();
});
});
it('save has type submit', () => {
mountComponent();
expect(findSaveButton().attributes('type')).toBe('submit');
});
describe('when isLoading is true', () => {
beforeEach(() => {
mountComponent({ isLoading: true });
});
it.each`
elementName
${'toggle'}
${'interval'}
${'schedule'}
${'latest'}
${'name-matching'}
`(`${FORM_ELEMENTS_ID_PREFIX}-$elementName is disabled`, ({ elementName }) => {
expect(findFormElements(elementName).attributes('disabled')).toBe('true');
});
it('submit button is disabled and shows a spinner', () => {
const button = findSaveButton();
expect(button.attributes('disabled')).toBeTruthy();
expect(findLoadingIcon(button)).toExist();
});
}); });
describe('form submit event ', () => { it.each`
it('calls the appropriate function', () => { elementName
mountComponent(); ${'toggle'}
findForm().trigger('submit'); ${'interval'}
expect(wrapper.emitted('submit')).toBeTruthy(); ${'schedule'}
}); ${'latest'}
${'name-matching'}
`(`${FORM_ELEMENTS_ID_PREFIX}-$elementName is disabled`, ({ elementName }) => {
expect(findFormElements(elementName).attributes('disabled')).toBe('true');
}); });
}); });
...@@ -196,20 +131,20 @@ describe('Expiration Policy Form', () => { ...@@ -196,20 +131,20 @@ describe('Expiration Policy Form', () => {
mountComponent({ value: { name_regex: invalidString } }); mountComponent({ value: { name_regex: invalidString } });
}); });
it('save btn is disabled', () => {
expect(findSaveButton().attributes('disabled')).toBeTruthy();
});
it('nameRegexState is false', () => { it('nameRegexState is false', () => {
expect(wrapper.vm.nameRegexState).toBe(false); expect(wrapper.vm.nameRegexState).toBe(false);
}); });
it('emit the @invalidated event', () => {
expect(wrapper.emitted('invalidated')).toBeTruthy();
});
}); });
it('if the user did not type validation is null', () => { it('if the user did not type validation is null', () => {
mountComponent({ value: { name_regex: '' } }); mountComponent({ value: { name_regex: '' } });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.nameRegexState).toBe(null); expect(wrapper.vm.nameRegexState).toBe(null);
expect(findSaveButton().attributes('disabled')).toBeFalsy(); expect(wrapper.emitted('validated')).toBeTruthy();
}); });
}); });
......
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