Commit 8d3d313c authored by Robert Hunt's avatar Robert Hunt Committed by Natalia Tepluhina

Create new create/update modals to show the form

parent d0429644
...@@ -6,6 +6,12 @@ export default { ...@@ -6,6 +6,12 @@ export default {
components: { components: {
GlButton, GlButton,
}, },
props: {
statusCheck: {
type: Object,
required: true,
},
},
i18n: { i18n: {
editButton: __('Edit'), editButton: __('Edit'),
removeButton: __('Remove...'), removeButton: __('Remove...'),
...@@ -15,7 +21,9 @@ export default { ...@@ -15,7 +21,9 @@ export default {
<template> <template>
<div class="gl-display-flex gl-justify-content-end"> <div class="gl-display-flex gl-justify-content-end">
<gl-button data-testid="edit-btn">{{ $options.i18n.editButton }}</gl-button> <gl-button data-testid="edit-btn" @click="$emit('open-update-modal', statusCheck)">
{{ $options.i18n.editButton }}
</gl-button>
<gl-button class="gl-ml-3" data-testid="remove-btn"> <gl-button class="gl-ml-3" data-testid="remove-btn">
{{ $options.i18n.removeButton }} {{ $options.i18n.removeButton }}
</gl-button> </gl-button>
......
...@@ -29,11 +29,6 @@ export default { ...@@ -29,11 +29,6 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
showValidation: {
type: Boolean,
required: false,
default: false,
},
statusCheck: { statusCheck: {
type: Object, type: Object,
required: false, required: false,
...@@ -41,51 +36,36 @@ export default { ...@@ -41,51 +36,36 @@ export default {
}, },
}, },
data() { data() {
const { protectedBranches, name, externalUrl: url } = this.statusCheck; const { protectedBranches: branches, name, externalUrl: url } = this.statusCheck;
return { return {
branches: protectedBranches, branches,
branchesToAdd: [], branchesToAdd: [],
branchesApiFailed: false, branchesApiFailed: false,
name, name,
showValidation: false,
url, url,
}; };
}, },
computed: { computed: {
formData() {
const { branches, name, url } = this;
return {
branches: branches.map(({ id }) => id),
name,
url,
};
},
isValid() { isValid() {
return this.isValidBranches && this.isValidName && this.isValidUrl; return this.nameState && this.urlState && this.branchesState;
},
isValidBranches() {
return this.branches.every((branch) => isEqual(branch, ANY_BRANCH) || isNumber(branch?.id));
},
isValidName() {
return Boolean(this.name);
},
isValidUrl() {
return Boolean(this.url) && isSafeURL(this.url);
}, },
branchesState() { branchesState() {
return !this.showValidation || this.isValidBranches; return !this.showValidation || this.checkBranchesValidity(this.branches);
}, },
nameState() { nameState() {
return ( return (
!this.showValidation || !this.showValidation ||
(this.isValidName && !this.serverValidationErrors.includes(NAME_TAKEN_SERVER_ERROR)) (this.checkNameValidity(this.name) &&
!this.serverValidationErrors.includes(NAME_TAKEN_SERVER_ERROR))
); );
}, },
urlState() { urlState() {
return ( return (
!this.showValidation || !this.showValidation ||
(this.isValidUrl && !this.serverValidationErrors.includes(URL_TAKEN_SERVER_ERROR)) (this.checkUrlValidity(this.url) &&
!this.serverValidationErrors.includes(URL_TAKEN_SERVER_ERROR))
); );
}, },
invalidNameMessage() { invalidNameMessage() {
...@@ -109,6 +89,15 @@ export default { ...@@ -109,6 +89,15 @@ export default {
}, },
}, },
methods: { methods: {
submit() {
this.showValidation = true;
if (this.isValid) {
const { branches, name, url } = this;
this.$emit('submit', { branches, name, url });
}
},
setBranchApiError(hasErrored, error) { setBranchApiError(hasErrored, error) {
if (!this.branchesApiFailed && error) { if (!this.branchesApiFailed && error) {
Sentry.captureException(error); Sentry.captureException(error);
...@@ -116,6 +105,15 @@ export default { ...@@ -116,6 +105,15 @@ export default {
this.branchesApiFailed = hasErrored; this.branchesApiFailed = hasErrored;
}, },
checkBranchesValidity(branches) {
return branches.every((branch) => isEqual(branch, ANY_BRANCH) || isNumber(branch?.id));
},
checkNameValidity(name) {
return Boolean(name);
},
checkUrlValidity(url) {
return Boolean(url) && isSafeURL(url);
},
}, },
i18n: { i18n: {
form: { form: {
...@@ -148,7 +146,7 @@ export default { ...@@ -148,7 +146,7 @@ export default {
<gl-alert v-if="branchesApiFailed" class="gl-mb-5" :dismissible="false" variant="danger"> <gl-alert v-if="branchesApiFailed" class="gl-mb-5" :dismissible="false" variant="danger">
{{ $options.i18n.validations.branchesApiFailure }} {{ $options.i18n.validations.branchesApiFailure }}
</gl-alert> </gl-alert>
<form novalidate> <form novalidate @submit.prevent.stop="submit">
<gl-form-group <gl-form-group
:label="$options.i18n.form.nameLabel" :label="$options.i18n.form.nameLabel"
:description="$options.i18n.form.nameDescription" :description="$options.i18n.form.nameDescription"
......
<script>
import { GlButton } from '@gitlab/ui';
import { mapActions } from 'vuex';
import { s__ } from '~/locale';
import SharedModal from './shared_modal.vue';
export default {
components: {
GlButton,
SharedModal,
},
methods: {
...mapActions(['postStatusCheck']),
show() {
this.$refs.modal.show();
},
},
modalId: 'status-checks-create-modal',
i18n: {
addButton: s__('StatusCheck|Add status check'),
title: s__('StatusCheck|Add status check'),
},
};
</script>
<template>
<div>
<gl-button category="secondary" variant="confirm" size="small" @click="show()">
{{ $options.i18n.addButton }}
</gl-button>
<shared-modal
ref="modal"
:modal-id="$options.modalId"
:title="$options.i18n.title"
:action="postStatusCheck"
/>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import { s__ } from '~/locale';
import SharedModal from './shared_modal.vue';
export default {
components: {
SharedModal,
},
props: {
statusCheck: {
type: Object,
required: true,
},
},
methods: {
...mapActions(['putStatusCheck']),
show() {
this.$refs.modal.show();
},
},
modalId: 'status-checks-update-modal',
i18n: {
title: s__('StatusCheck|Update status check'),
},
};
</script>
<template>
<shared-modal
ref="modal"
:modal-id="$options.modalId"
:title="$options.i18n.title"
:status-check="statusCheck"
:action="putStatusCheck"
/>
</template>
<script>
import { GlModal, GlModalDirective } from '@gitlab/ui';
import { mapState } from 'vuex';
import { __ } from '~/locale';
import { modalPrimaryActionProps } from '../utils';
import StatusCheckForm from './form.vue';
const i18n = { cancelButton: __('Cancel') };
export default {
components: {
GlModal,
StatusCheckForm,
},
directives: {
GlModal: GlModalDirective,
},
props: {
action: {
type: Function,
required: true,
},
modalId: {
type: String,
required: true,
},
statusCheck: {
type: Object,
required: false,
default: undefined,
},
title: {
type: String,
required: true,
},
},
data() {
return {
serverValidationErrors: [],
submitting: false,
};
},
computed: {
...mapState({
projectId: ({ settings }) => settings.projectId,
}),
primaryActionProps() {
return modalPrimaryActionProps(this.title, this.submitting);
},
},
methods: {
async submit() {
this.$refs.form.submit();
},
async handleFormSubmit(formData) {
this.submitting = true;
const { branches, name, url } = formData;
try {
await this.action({
externalUrl: url,
id: this.statusCheck?.id,
name,
protectedBranchIds: branches.map(({ id }) => id),
});
this.$refs.modal.hide();
} catch (failureResponse) {
this.serverValidationErrors = failureResponse?.response?.data?.message || [];
}
this.submitting = false;
},
show() {
this.$refs.modal.show();
},
resetModal() {
this.serverValidationErrors = [];
},
},
cancelActionProps: {
text: i18n.cancelButton,
},
};
</script>
<template>
<gl-modal
ref="modal"
:modal-id="modalId"
:title="title"
:action-primary="primaryActionProps"
:action-cancel="$options.cancelActionProps"
size="sm"
@ok.prevent="submit"
@hidden="resetModal"
>
<status-check-form
ref="form"
:project-id="projectId"
:server-validation-errors="serverValidationErrors"
:status-check="statusCheck"
@submit="handleFormSubmit"
/>
</gl-modal>
</template>
<script> <script>
import { GlButton, GlTable } from '@gitlab/ui'; import { GlTable } from '@gitlab/ui';
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants'; import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants';
import { thWidthClass } from '~/lib/utils/table_utility'; import { thWidthClass } from '~/lib/utils/table_utility';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { EMPTY_STATUS_CHECK } from '../constants';
import Actions from './actions.vue'; import Actions from './actions.vue';
import Branch from './branch.vue'; import Branch from './branch.vue';
import ModalCreate from './modal_create.vue';
import ModalUpdate from './modal_update.vue';
export const i18n = { export const i18n = {
addButton: s__('StatusCheck|Add status check'),
apiHeader: __('API'), apiHeader: __('API'),
branchHeader: __('Target branch'), branchHeader: __('Target branch'),
emptyTableText: s__('StatusCheck|No status checks are defined yet.'), emptyTableText: s__('StatusCheck|No status checks are defined yet.'),
...@@ -19,12 +21,24 @@ export default { ...@@ -19,12 +21,24 @@ export default {
components: { components: {
Actions, Actions,
Branch, Branch,
GlButton,
GlTable, GlTable,
ModalCreate,
ModalUpdate,
},
data() {
return {
statusCheckToUpdate: EMPTY_STATUS_CHECK,
};
}, },
computed: { computed: {
...mapState(['statusChecks']), ...mapState(['statusChecks']),
}, },
methods: {
openUpdateModal(statusCheck) {
this.statusCheckToUpdate = statusCheck;
this.$refs.updateModal.show();
},
},
fields: [ fields: [
{ {
key: 'name', key: 'name',
...@@ -65,13 +79,12 @@ export default { ...@@ -65,13 +79,12 @@ export default {
<template #cell(protectedBranches)="{ item }"> <template #cell(protectedBranches)="{ item }">
<branch :branches="item.protectedBranches" /> <branch :branches="item.protectedBranches" />
</template> </template>
<template #cell(actions)> <template #cell(actions)="{ item }">
<actions /> <actions :status-check="item" @open-update-modal="openUpdateModal" />
</template> </template>
</gl-table> </gl-table>
<gl-button category="secondary" variant="confirm" size="small"> <modal-create />
{{ $options.i18n.addButton }} <modal-update ref="updateModal" :status-check="statusCheckToUpdate" />
</gl-button>
</div> </div>
</template> </template>
...@@ -9,7 +9,7 @@ export const ANY_BRANCH = { ...@@ -9,7 +9,7 @@ export const ANY_BRANCH = {
export const EMPTY_STATUS_CHECK = { export const EMPTY_STATUS_CHECK = {
name: '', name: '',
protectedBranches: [], protectedBranches: [],
url: '', externalUrl: '',
}; };
export const URL_TAKEN_SERVER_ERROR = 'External url has already been taken'; export const URL_TAKEN_SERVER_ERROR = 'External url has already been taken';
......
export const modalPrimaryActionProps = (text, loading) => ({
text,
attributes: [{ variant: 'confirm', loading }],
});
...@@ -2,17 +2,31 @@ import { GlButton } from '@gitlab/ui'; ...@@ -2,17 +2,31 @@ import { GlButton } from '@gitlab/ui';
import Actions from 'ee/status_checks/components/actions.vue'; import Actions from 'ee/status_checks/components/actions.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
const statusCheck = {
externalUrl: 'https://foo.com',
id: 1,
name: 'Foo',
protectedBranches: [],
};
describe('Status checks actions', () => { describe('Status checks actions', () => {
let wrapper; let wrapper;
const createWrapper = () => { const createWrapper = () => {
wrapper = shallowMountExtended(Actions, { wrapper = shallowMountExtended(Actions, {
propsData: {
statusCheck,
},
stubs: { stubs: {
GlButton, GlButton,
}, },
}); });
}; };
beforeEach(() => {
createWrapper();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -20,15 +34,19 @@ describe('Status checks actions', () => { ...@@ -20,15 +34,19 @@ describe('Status checks actions', () => {
const findEditBtn = () => wrapper.findByTestId('edit-btn'); const findEditBtn = () => wrapper.findByTestId('edit-btn');
const findRemoveBtn = () => wrapper.findByTestId('remove-btn'); const findRemoveBtn = () => wrapper.findByTestId('remove-btn');
describe('Edit button', () => {
it('renders the edit button', () => { it('renders the edit button', () => {
createWrapper();
expect(findEditBtn().text()).toBe('Edit'); expect(findEditBtn().text()).toBe('Edit');
}); });
it('renders the remove button', () => { it('sends the status check to the update event', () => {
createWrapper(); findEditBtn().trigger('click');
expect(wrapper.emitted('open-update-modal')[0][0]).toStrictEqual(statusCheck);
});
});
it('renders the remove button', () => {
expect(findRemoveBtn().text()).toBe('Remove...'); expect(findRemoveBtn().text()).toBe('Remove...');
}); });
}); });
import { GlAlert, GlFormGroup, GlFormInput } from '@gitlab/ui'; import { GlAlert, GlFormGroup, GlFormInput } from '@gitlab/ui';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { nextTick } from 'vue';
import BranchesSelect from 'ee/status_checks/components/branches_select.vue'; import BranchesSelect from 'ee/status_checks/components/branches_select.vue';
import Form from 'ee/status_checks/components/form.vue'; import Form from 'ee/status_checks/components/form.vue';
import { NAME_TAKEN_SERVER_ERROR, URL_TAKEN_SERVER_ERROR } from 'ee/status_checks/constants'; import { NAME_TAKEN_SERVER_ERROR, URL_TAKEN_SERVER_ERROR } from 'ee/status_checks/constants';
...@@ -11,7 +10,6 @@ import { TEST_PROTECTED_BRANCHES } from '../mock_data'; ...@@ -11,7 +10,6 @@ import { TEST_PROTECTED_BRANCHES } from '../mock_data';
const projectId = '1'; const projectId = '1';
const statusCheck = { const statusCheck = {
protectedBranches: TEST_PROTECTED_BRANCHES, protectedBranches: TEST_PROTECTED_BRANCHES,
branches: TEST_PROTECTED_BRANCHES,
name: 'Foo', name: 'Foo',
externalUrl: 'https://foo.com', externalUrl: 'https://foo.com',
}; };
...@@ -19,10 +17,15 @@ const sentryError = new Error('Network error'); ...@@ -19,10 +17,15 @@ const sentryError = new Error('Network error');
describe('Status checks form', () => { describe('Status checks form', () => {
let wrapper; let wrapper;
const submitHandler = jest.fn();
const createWrapper = (props = {}) => { const createWrapper = (props = {}) => {
wrapper = shallowMountExtended(Form, { wrapper = shallowMountExtended(Form, {
propsData: { projectId, ...props }, propsData: {
projectId,
submitHandler,
...props,
},
stubs: { stubs: {
GlFormGroup: stubComponent(GlFormGroup, { GlFormGroup: stubComponent(GlFormGroup, {
props: ['state', 'invalidFeedback'], props: ['state', 'invalidFeedback'],
...@@ -31,10 +34,12 @@ describe('Status checks form', () => { ...@@ -31,10 +34,12 @@ describe('Status checks form', () => {
props: ['state', 'disabled', 'value'], props: ['state', 'disabled', 'value'],
template: `<input />`, template: `<input />`,
}), }),
BranchesSelect: stubComponent(BranchesSelect),
}, },
}); });
}; };
const findForm = () => wrapper.find('form');
const findNameInput = () => wrapper.findByTestId('name'); const findNameInput = () => wrapper.findByTestId('name');
const findNameValidation = () => wrapper.findByTestId('name-group'); const findNameValidation = () => wrapper.findByTestId('name-group');
const findBranchesSelect = () => wrapper.findComponent(BranchesSelect); const findBranchesSelect = () => wrapper.findComponent(BranchesSelect);
...@@ -61,7 +66,7 @@ describe('Status checks form', () => { ...@@ -61,7 +66,7 @@ describe('Status checks form', () => {
expect(inputsAreValid()).toBe(true); expect(inputsAreValid()).toBe(true);
expect(findNameInput().props('value')).toBe(''); expect(findNameInput().props('value')).toBe('');
expect(findBranchesSelect().props('selectedBranches')).toStrictEqual([]); expect(findBranchesSelect().props('selectedBranches')).toStrictEqual([]);
expect(findUrlInput().props('value')).toBe(undefined); expect(findUrlInput().props('value')).toBe('');
}); });
it('shows filled inputs when initial data is given', () => { it('shows filled inputs when initial data is given', () => {
...@@ -69,15 +74,20 @@ describe('Status checks form', () => { ...@@ -69,15 +74,20 @@ describe('Status checks form', () => {
expect(inputsAreValid()).toBe(true); expect(inputsAreValid()).toBe(true);
expect(findNameInput().props('value')).toBe(statusCheck.name); expect(findNameInput().props('value')).toBe(statusCheck.name);
expect(findBranchesSelect().props('selectedBranches')).toStrictEqual(statusCheck.branches); expect(findBranchesSelect().props('selectedBranches')).toStrictEqual(
statusCheck.protectedBranches,
);
expect(findUrlInput().props('value')).toBe(statusCheck.externalUrl); expect(findUrlInput().props('value')).toBe(statusCheck.externalUrl);
}); });
}); });
describe('Validation', () => { describe('Validation', () => {
it('shows the validation messages if showValidation is passed', () => { it('shows the validation messages if invalid on submission', async () => {
createWrapper({ showValidation: true, branches: ['abc'] }); createWrapper({ branches: ['abc'] });
await findForm().trigger('submit');
expect(wrapper.emitted('submit')).toBe(undefined);
expect(inputsAreValid()).toBe(false); expect(inputsAreValid()).toBe(false);
expect(findNameValidation().props('invalidFeedback')).toBe('Please provide a name.'); expect(findNameValidation().props('invalidFeedback')).toBe('Please provide a name.');
expect(findBranchesValidation().props('invalidFeedback')).toBe( expect(findBranchesValidation().props('invalidFeedback')).toBe(
...@@ -86,22 +96,26 @@ describe('Status checks form', () => { ...@@ -86,22 +96,26 @@ describe('Status checks form', () => {
expect(findUrlValidation().props('invalidFeedback')).toBe('Please provide a valid URL.'); expect(findUrlValidation().props('invalidFeedback')).toBe('Please provide a valid URL.');
}); });
it('shows the invalid URL error if the URL is unsafe', () => { it('shows the invalid URL error if the URL is unsafe', async () => {
createWrapper({ createWrapper({
showValidation: true,
statusCheck: { ...statusCheck, externalUrl: 'ftp://foo.com' }, statusCheck: { ...statusCheck, externalUrl: 'ftp://foo.com' },
}); });
await findForm().trigger('submit');
expect(wrapper.emitted('submit')).toBe(undefined);
expect(inputsAreValid()).toBe(false); expect(inputsAreValid()).toBe(false);
expect(findUrlValidation().props('invalidFeedback')).toBe('Please provide a valid URL.'); expect(findUrlValidation().props('invalidFeedback')).toBe('Please provide a valid URL.');
}); });
it('shows the serverValidationErrors if given', () => { it('shows the serverValidationErrors if given', async () => {
createWrapper({ createWrapper({
showValidation: true,
serverValidationErrors: [NAME_TAKEN_SERVER_ERROR, URL_TAKEN_SERVER_ERROR], serverValidationErrors: [NAME_TAKEN_SERVER_ERROR, URL_TAKEN_SERVER_ERROR],
}); });
await findForm().trigger('submit');
expect(wrapper.emitted('submit')).toBe(undefined);
expect(inputsAreValid()).toBe(false); expect(inputsAreValid()).toBe(false);
expect(findNameValidation().props('invalidFeedback')).toBe('Name is already taken.'); expect(findNameValidation().props('invalidFeedback')).toBe('Name is already taken.');
expect(findUrlValidation().props('invalidFeedback')).toBe( expect(findUrlValidation().props('invalidFeedback')).toBe(
...@@ -109,9 +123,18 @@ describe('Status checks form', () => { ...@@ -109,9 +123,18 @@ describe('Status checks form', () => {
); );
}); });
it('does not show any errors if the values are valid', () => { it('does not show any errors if the values are valid', async () => {
createWrapper({ showValidation: true, statusCheck }); createWrapper({ statusCheck });
await findForm().trigger('submit');
expect(wrapper.emitted('submit')).toContainEqual([
{
branches: statusCheck.protectedBranches,
name: statusCheck.name,
url: statusCheck.externalUrl,
},
]);
expect(inputsAreValid()).toBe(true); expect(inputsAreValid()).toBe(true);
}); });
}); });
...@@ -131,19 +154,16 @@ describe('Status checks form', () => { ...@@ -131,19 +154,16 @@ describe('Status checks form', () => {
it('shows the alert', async () => { it('shows the alert', async () => {
expect(findBranchesErrorAlert().exists()).toBe(false); expect(findBranchesErrorAlert().exists()).toBe(false);
findBranchesSelect().vm.$emit('apiError', true, sentryError); await findBranchesSelect().vm.$emit('apiError', true, sentryError);
await nextTick();
expect(findBranchesErrorAlert().exists()).toBe(true); expect(findBranchesErrorAlert().exists()).toBe(true);
}); });
it('hides the alert if the apiError is reset', async () => { it('hides the alert if the apiError is reset', async () => {
findBranchesSelect().vm.$emit('apiError', true, sentryError); await findBranchesSelect().vm.$emit('apiError', true, sentryError);
await nextTick();
expect(findBranchesErrorAlert().exists()).toBe(true); expect(findBranchesErrorAlert().exists()).toBe(true);
findBranchesSelect().vm.$emit('apiError', false); await findBranchesSelect().vm.$emit('apiError', false);
await nextTick();
expect(findBranchesErrorAlert().exists()).toBe(false); expect(findBranchesErrorAlert().exists()).toBe(false);
}); });
......
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import ModalCreate from 'ee/status_checks/components/modal_create.vue';
import SharedModal from 'ee/status_checks/components/shared_modal.vue';
Vue.use(Vuex);
const projectId = '1';
const statusChecksPath = '/api/v4/projects/1/external_approval_rules';
const modalId = 'status-checks-create-modal';
const title = 'Add status check';
describe('Modal create', () => {
let wrapper;
let store;
const actions = {
postStatusCheck: jest.fn(),
};
const createWrapper = () => {
store = new Vuex.Store({
actions,
state: {
isLoading: false,
settings: { projectId, statusChecksPath },
statusChecks: [],
},
});
wrapper = shallowMount(ModalCreate, {
store,
stubs: {
GlButton,
},
});
wrapper.vm.$refs.modal.show = jest.fn();
};
beforeEach(() => {
createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
const findAddBtn = () => wrapper.findComponent(GlButton);
const findModal = () => wrapper.findComponent(SharedModal);
describe('Add button', () => {
it('renders', () => {
expect(findAddBtn().text()).toBe('Add status check');
});
it('opens the modal', () => {
findAddBtn().trigger('click');
expect(wrapper.vm.$refs.modal.show).toHaveBeenCalled();
});
});
describe('Modal', () => {
it('sets the modals props', () => {
expect(findModal().props()).toStrictEqual({
action: expect.any(Function),
modalId,
title,
statusCheck: undefined,
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import ModalUpdate from 'ee/status_checks/components/modal_update.vue';
import SharedModal from 'ee/status_checks/components/shared_modal.vue';
import { TEST_PROTECTED_BRANCHES } from '../mock_data';
Vue.use(Vuex);
const projectId = '1';
const statusChecksPath = '/api/v4/projects/1/external_approval_rules';
const modalId = 'status-checks-update-modal';
const title = 'Update status check';
const statusCheck = {
externalUrl: 'https://foo.com',
id: 1,
name: 'Foo',
protectedBranches: TEST_PROTECTED_BRANCHES,
};
describe('Modal update', () => {
let wrapper;
let store;
const actions = {
putStatusCheck: jest.fn(),
};
const createWrapper = () => {
store = new Vuex.Store({
actions,
state: {
isLoading: false,
settings: { projectId, statusChecksPath },
statusChecks: [],
},
});
wrapper = shallowMount(ModalUpdate, {
propsData: {
statusCheck,
},
store,
});
};
beforeEach(() => {
createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
const findModal = () => wrapper.findComponent(SharedModal);
describe('Modal', () => {
it('sets the modals props', () => {
expect(findModal().props()).toStrictEqual({
action: expect.any(Function),
modalId,
title,
statusCheck,
});
});
});
});
import { GlButton, GlModal } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import Form from 'ee/status_checks/components/form.vue';
import SharedModal from 'ee/status_checks/components/shared_modal.vue';
import { EMPTY_STATUS_CHECK } from 'ee/status_checks/constants';
import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_PROTECTED_BRANCHES } from '../mock_data';
Vue.use(Vuex);
const projectId = '1';
const statusChecksPath = '/api/v4/projects/1/external_approval_rules';
const modalId = 'modal-id';
const title = 'Modal title';
const statusCheck = {
externalUrl: 'https://foo.com',
id: 1,
name: 'Foo',
protectedBranches: TEST_PROTECTED_BRANCHES,
};
const formData = {
branches: statusCheck.protectedBranches,
name: statusCheck.name,
url: statusCheck.externalUrl,
};
describe('Shared modal', () => {
let wrapper;
let store;
const glModalDirective = jest.fn();
const action = jest.fn();
const createWrapper = (props = {}) => {
store = new Vuex.Store({
state: {
isLoading: false,
settings: { projectId, statusChecksPath },
statusChecks: [],
},
});
wrapper = shallowMountExtended(SharedModal, {
directives: {
glModal: {
bind(el, { modifiers }) {
glModalDirective(modifiers);
},
},
},
propsData: {
action,
modalId,
title,
...props,
},
store,
stubs: {
GlButton: stubComponent(GlButton, {
props: ['v-gl-modal', 'loading'],
}),
},
});
wrapper.vm.$refs.modal.hide = jest.fn();
wrapper.vm.$refs.form.submit = jest.fn();
};
afterEach(() => {
wrapper.destroy();
});
const findModal = () => wrapper.findComponent(GlModal);
const findForm = () => wrapper.findComponent(Form);
describe('Modal', () => {
describe('defaults', () => {
beforeEach(() => {
createWrapper();
});
it('sets the modals props', () => {
expect(findModal().props()).toMatchObject({
actionPrimary: { text: title, attributes: [{ variant: 'confirm', loading: false }] },
actionCancel: { text: 'Cancel' },
modalId,
size: 'sm',
title,
});
});
});
describe.each`
given | expected
${undefined} | ${EMPTY_STATUS_CHECK}
${statusCheck} | ${statusCheck}
`('when the $given status check is passed', ({ given, expected }) => {
beforeEach(() => {
createWrapper({ statusCheck: given });
});
it('shows the form with the correct props', () => {
expect(findForm().props()).toMatchObject({
projectId,
serverValidationErrors: [],
statusCheck: expected,
});
});
});
});
describe('Submission', () => {
describe.each`
given | expected
${undefined} | ${EMPTY_STATUS_CHECK}
${statusCheck} | ${statusCheck}
`('when the $given status check is passed', ({ given, expected }) => {
beforeEach(() => {
createWrapper({ statusCheck: given });
});
it('submits the values and hides the modal', async () => {
await findModal().vm.$emit('ok', { preventDefault: () => null });
await findForm().vm.$emit('submit', formData);
await waitForPromises();
expect(wrapper.vm.$refs.form.submit).toHaveBeenCalled();
expect(action).toHaveBeenCalledWith({
externalUrl: formData.url,
id: expected?.id,
name: formData.name,
protectedBranchIds: formData.branches.map(({ id }) => id),
});
expect(wrapper.vm.$refs.modal.hide).toHaveBeenCalled();
});
it('submits the values, the API fails and does not hide the modal', async () => {
const message = ['Name has already been taken'];
action.mockRejectedValueOnce({
response: { data: { message } },
});
await findModal().vm.$emit('ok', { preventDefault: () => null });
await findForm().vm.$emit('submit', formData);
await waitForPromises();
expect(wrapper.vm.$refs.form.submit).toHaveBeenCalled();
expect(action).toHaveBeenCalledWith({
externalUrl: formData.url,
id: expected?.id,
name: formData.name,
protectedBranchIds: formData.branches.map(({ id }) => id),
});
expect(wrapper.vm.$refs.modal.hide).not.toHaveBeenCalled();
expect(findForm().props()).toMatchObject({
projectId,
serverValidationErrors: message,
statusCheck: expected,
});
});
});
});
});
import { GlButton, GlTable } from '@gitlab/ui'; import { GlTable } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import Actions from 'ee/status_checks/components/actions.vue'; import Actions from 'ee/status_checks/components/actions.vue';
import Branch from 'ee/status_checks/components/branch.vue'; import Branch from 'ee/status_checks/components/branch.vue';
import ModalCreate from 'ee/status_checks/components/modal_create.vue';
import ModalUpdate from 'ee/status_checks/components/modal_update.vue';
import StatusChecks, { i18n } from 'ee/status_checks/components/status_checks.vue'; import StatusChecks, { i18n } from 'ee/status_checks/components/status_checks.vue';
import createStore from 'ee/status_checks/store'; import createStore from 'ee/status_checks/store';
import { SET_STATUS_CHECKS } from 'ee/status_checks/store/mutation_types'; import { SET_STATUS_CHECKS } from 'ee/status_checks/store/mutation_types';
Vue.use(Vuex); Vue.use(Vuex);
const statusChecks = [
{ name: 'Foo', externalUrl: 'http://foo.com/api', protectedBranches: [] },
{ name: 'Bar', externalUrl: 'http://bar.com/api', protectedBranches: [{ name: 'main' }] },
];
describe('Status checks', () => { describe('Status checks', () => {
let store; let store;
let wrapper; let wrapper;
...@@ -17,13 +24,16 @@ describe('Status checks', () => { ...@@ -17,13 +24,16 @@ describe('Status checks', () => {
const createWrapper = (mountFn = mount) => { const createWrapper = (mountFn = mount) => {
store = createStore(); store = createStore();
wrapper = mountFn(StatusChecks, { store }); wrapper = mountFn(StatusChecks, { store });
wrapper.vm.$refs.updateModal.show = jest.fn();
}; };
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
const findAddButton = () => wrapper.findComponent(GlButton); const findCreateModal = () => wrapper.findComponent(ModalCreate);
const findUpdateModal = () => wrapper.findComponent(ModalUpdate);
const findTable = () => wrapper.findComponent(GlTable); const findTable = () => wrapper.findComponent(GlTable);
const findHeaders = () => findTable().find('thead').find('tr').findAll('th'); const findHeaders = () => findTable().find('thead').find('tr').findAll('th');
const findBranch = (trIdx) => wrapper.findAllComponents(Branch).at(trIdx); const findBranch = (trIdx) => wrapper.findAllComponents(Branch).at(trIdx);
...@@ -45,19 +55,20 @@ describe('Status checks', () => { ...@@ -45,19 +55,20 @@ describe('Status checks', () => {
expect(findCell(0, 0).text()).toBe(i18n.emptyTableText); expect(findCell(0, 0).text()).toBe(i18n.emptyTableText);
}); });
it('renders the add button', () => { it('creates the create modal', () => {
createWrapper(shallowMount); createWrapper(shallowMount);
expect(findAddButton().text()).toBe(i18n.addButton); expect(findCreateModal().exists()).toBe(true);
});
}); });
describe('Filled table', () => { it('creates the update modal', () => {
const statusChecks = [ createWrapper(shallowMount);
{ name: 'Foo', externalUrl: 'http://foo.com/api', protectedBranches: [] },
{ name: 'Bar', externalUrl: 'http://bar.com/api', protectedBranches: [{ name: 'main' }] }, expect(findUpdateModal().exists()).toBe(true);
]; });
});
describe('Table', () => {
beforeEach(() => { beforeEach(() => {
createWrapper(); createWrapper();
store.commit(SET_STATUS_CHECKS, statusChecks); store.commit(SET_STATUS_CHECKS, statusChecks);
...@@ -87,8 +98,36 @@ describe('Status checks', () => { ...@@ -87,8 +98,36 @@ describe('Status checks', () => {
}); });
it('renders the actions', () => { it('renders the actions', () => {
expect(findActions(index, 1).exists()).toBe(true); expect(findActions(index, 1).props('statusCheck')).toStrictEqual(statusCheck);
});
});
}); });
describe('Update modal filling', () => {
beforeEach(() => {
createWrapper();
store.commit(SET_STATUS_CHECKS, statusChecks);
});
it('opens the update modal with the correct status check when an edit button is clicked', async () => {
const statusCheck = findActions(0, 1).props('statusCheck');
await findActions(0, 1).vm.$emit('open-update-modal', statusCheck);
expect(findUpdateModal().props('statusCheck')).toStrictEqual(statusCheck);
expect(wrapper.vm.$refs.updateModal.show).toHaveBeenCalled();
});
it('updates the status check prop for the update modal when another edit button is clicked', async () => {
const statusCheck = findActions(1, 1).props('statusCheck');
await findActions(0, 1).vm.$emit(
'update-status-to-check',
findActions(0, 1).props('statusCheck'),
);
await findActions(1, 1).vm.$emit('open-update-modal', statusCheck);
expect(findUpdateModal().props('statusCheck')).toStrictEqual(statusCheck);
}); });
}); });
}); });
import * as Utils from 'ee/status_checks/utils';
describe('modalPrimaryActionProps', () => {
it('returns the props with the text and loading state', () => {
const text = 'Button text';
const loading = true;
expect(Utils.modalPrimaryActionProps(text, loading)).toStrictEqual({
text,
attributes: [{ variant: 'confirm', loading }],
});
});
});
...@@ -31147,6 +31147,9 @@ msgstr "" ...@@ -31147,6 +31147,9 @@ msgstr ""
msgid "StatusCheck|Target branch" msgid "StatusCheck|Target branch"
msgstr "" msgstr ""
msgid "StatusCheck|Update status check"
msgstr ""
msgid "StatusCheck|You are about to remove the %{name} status check." msgid "StatusCheck|You are about to remove the %{name} status check."
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