Commit 9c59b0e2 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch '342121-move-remaining-events-to-vue-components' into 'master'

Move "test settings" code from integration_settings_form.js to Vue component

See merge request gitlab-org/gitlab!76291
parents 9709e437 aaa16315
import { s__, __ } from '~/locale';
export const TEST_INTEGRATION_EVENT = 'testIntegration';
export const SAVE_INTEGRATION_EVENT = 'saveIntegration';
export const VALIDATE_INTEGRATION_FORM_EVENT = 'validateIntegrationForm';
......
<script>
import { GlButton, GlModalDirective, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { mapState, mapActions, mapGetters } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
TEST_INTEGRATION_EVENT,
SAVE_INTEGRATION_EVENT,
VALIDATE_INTEGRATION_FORM_EVENT,
I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
integrationLevels,
} from '~/integrations/constants';
import eventHub from '../event_hub';
import { testIntegrationSettings } from '../api';
import ActiveCheckbox from './active_checkbox.vue';
import ConfirmationModal from './confirmation_modal.vue';
import DynamicField from './dynamic_field.vue';
......@@ -50,18 +54,12 @@ export default {
data() {
return {
integrationActive: false,
testingLoading: false,
};
},
computed: {
...mapGetters(['currentKey', 'propsSource', 'isDisabled']),
...mapState([
'defaultState',
'customState',
'override',
'isSaving',
'isTesting',
'isResetting',
]),
...mapState(['defaultState', 'customState', 'override', 'isSaving', 'isResetting']),
isEditable() {
return this.propsSource.editable;
},
......@@ -74,9 +72,18 @@ export default {
this.customState.integrationLevel === integrationLevels.GROUP
);
},
showReset() {
showResetButton() {
return this.isInstanceOrGroupLevel && this.propsSource.resetPath;
},
showTestButton() {
return this.propsSource.canTest;
},
disableSaveButton() {
return Boolean(this.isResetting || this.testingLoading);
},
disableResetButton() {
return Boolean(this.isSaving || this.testingLoading);
},
},
mounted() {
// this form element is defined in Haml
......@@ -86,7 +93,6 @@ export default {
...mapActions([
'setOverride',
'setIsSaving',
'setIsTesting',
'setIsResetting',
'fetchResetIntegration',
'requestJiraIssueTypes',
......@@ -98,17 +104,39 @@ export default {
eventHub.$emit(SAVE_INTEGRATION_EVENT, formValid);
},
onTestClick() {
this.setIsTesting(true);
this.testingLoading = true;
const formValid = this.form.checkValidity();
eventHub.$emit(TEST_INTEGRATION_EVENT, formValid);
if (!this.form.checkValidity()) {
eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
return;
}
testIntegrationSettings(this.propsSource.testPath, this.getFormData())
.then(({ data: { error, message = I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE } }) => {
if (error) {
eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
this.$toast.show(message);
return;
}
this.$toast.show(I18N_SUCCESSFUL_CONNECTION_MESSAGE);
})
.catch((error) => {
this.$toast.show(I18N_DEFAULT_ERROR_MESSAGE);
Sentry.captureException(error);
})
.finally(() => {
this.testingLoading = false;
});
},
onResetClick() {
this.fetchResetIntegration();
},
onRequestJiraIssueTypes() {
const formData = new FormData(this.form);
this.requestJiraIssueTypes(formData);
this.requestJiraIssueTypes(this.getFormData());
},
getFormData() {
return new FormData(this.form);
},
onToggleIntegrationState(integrationActive) {
this.integrationActive = integrationActive;
......@@ -183,7 +211,7 @@ export default {
category="primary"
variant="confirm"
:loading="isSaving"
:disabled="isDisabled"
:disabled="disableSaveButton"
data-qa-selector="save_changes_button"
>
{{ __('Save changes') }}
......@@ -196,7 +224,7 @@ export default {
variant="confirm"
type="submit"
:loading="isSaving"
:disabled="isDisabled"
:disabled="disableSaveButton"
data-testid="save-button"
data-qa-selector="save_changes_button"
@click.prevent="onSaveClick"
......@@ -205,25 +233,24 @@ export default {
</gl-button>
<gl-button
v-if="propsSource.canTest"
v-if="showTestButton"
category="secondary"
variant="confirm"
:loading="isTesting"
:loading="testingLoading"
:disabled="isDisabled"
:href="propsSource.testPath"
data-testid="test-button"
@click.prevent="onTestClick"
>
{{ __('Test settings') }}
</gl-button>
<template v-if="showReset">
<template v-if="showResetButton">
<gl-button
v-gl-modal.confirmResetIntegration
category="secondary"
variant="confirm"
:loading="isResetting"
:disabled="isDisabled"
:disabled="disableResetButton"
data-testid="reset-button"
>
{{ __('Reset') }}
......
......@@ -11,7 +11,6 @@ import * as types from './mutation_types';
export const setOverride = ({ commit }, override) => commit(types.SET_OVERRIDE, override);
export const setIsSaving = ({ commit }, isSaving) => commit(types.SET_IS_SAVING, isSaving);
export const setIsTesting = ({ commit }, isTesting) => commit(types.SET_IS_TESTING, isTesting);
export const setIsResetting = ({ commit }, isResetting) =>
commit(types.SET_IS_RESETTING, isResetting);
......
export const isInheriting = (state) => (state.defaultState === null ? false : !state.override);
export const isDisabled = (state) => state.isSaving || state.isTesting || state.isResetting;
export const isDisabled = (state) => state.isSaving || state.isResetting;
export const propsSource = (state, getters) =>
getters.isInheriting ? state.defaultState : state.customState;
......
export const SET_OVERRIDE = 'SET_OVERRIDE';
export const SET_IS_SAVING = 'SET_IS_SAVING';
export const SET_IS_TESTING = 'SET_IS_TESTING';
export const SET_IS_RESETTING = 'SET_IS_RESETTING';
export const SET_IS_LOADING_JIRA_ISSUE_TYPES = 'SET_IS_LOADING_JIRA_ISSUE_TYPES';
......
......@@ -7,9 +7,6 @@ export default {
[types.SET_IS_SAVING](state, isSaving) {
state.isSaving = isSaving;
},
[types.SET_IS_TESTING](state, isTesting) {
state.isTesting = isTesting;
},
[types.SET_IS_RESETTING](state, isResetting) {
state.isResetting = isResetting;
},
......
......@@ -6,7 +6,6 @@ export default ({ defaultState = null, customState = {} } = {}) => {
defaultState,
customState,
isSaving: false,
isTesting: false,
isResetting: false,
isLoadingJiraIssueTypes: false,
loadingJiraIssueTypesErrorMessage: '',
......
import { delay } from 'lodash';
import toast from '~/vue_shared/plugins/global_toast';
import initForm from './edit';
import eventHub from './edit/event_hub';
import {
TEST_INTEGRATION_EVENT,
SAVE_INTEGRATION_EVENT,
VALIDATE_INTEGRATION_FORM_EVENT,
I18N_DEFAULT_ERROR_MESSAGE,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
} from './constants';
import { testIntegrationSettings } from './edit/api';
import { SAVE_INTEGRATION_EVENT, VALIDATE_INTEGRATION_FORM_EVENT } from './constants';
export default class IntegrationSettingsForm {
constructor(formSelector) {
......@@ -29,9 +21,6 @@ export default class IntegrationSettingsForm {
document.querySelector('.js-vue-default-integration-settings'),
this.formSelector,
);
eventHub.$on(TEST_INTEGRATION_EVENT, (formValid) => {
this.testIntegration(formValid);
});
eventHub.$on(SAVE_INTEGRATION_EVENT, (formValid) => {
this.saveIntegration(formValid);
});
......@@ -53,47 +42,4 @@ export default class IntegrationSettingsForm {
this.vue.$store.dispatch('setIsSaving', false);
}
}
testIntegration(formValid) {
// Service was marked active so now we check;
// 1) If form contents are valid
// 2) If this service can be tested
// If both conditions are true, we override form submission
// and test the service using provided configuration.
if (formValid) {
this.testSettings(new FormData(this.$form));
} else {
eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
this.vue.$store.dispatch('setIsTesting', false);
}
}
/**
* Get a list of Jira issue types for the currently configured project
*
* @param {string} formData - URL encoded string containing the form data
*
* @return {Promise}
*/
/**
* Test Integration config
*/
testSettings(formData) {
return testIntegrationSettings(this.testEndPoint, formData)
.then(({ data }) => {
if (data.error) {
toast(`${data.message} ${data.service_response}`);
} else {
this.vue.$store.dispatch('receiveJiraIssueTypesSuccess', data.issuetypes);
toast(I18N_SUCCESSFUL_CONNECTION_MESSAGE);
}
})
.catch(() => {
toast(I18N_DEFAULT_ERROR_MESSAGE);
})
.finally(() => {
this.vue.$store.dispatch('setIsTesting', false);
});
}
}
......@@ -81,12 +81,7 @@ export default {
},
computed: {
...mapGetters(['isInheriting']),
...mapState([
'isTesting',
'jiraIssueTypes',
'isLoadingJiraIssueTypes',
'loadingJiraIssueTypesErrorMessage',
]),
...mapState(['jiraIssueTypes', 'isLoadingJiraIssueTypes', 'loadingJiraIssueTypesErrorMessage']),
checkboxDisabled() {
return !this.showFullFeature || this.isInheriting;
},
......@@ -180,7 +175,7 @@ export default {
<gl-dropdown
class="gl-w-full"
:disabled="!jiraIssueTypes.length"
:loading="isLoadingJiraIssueTypes || isTesting"
:loading="isLoadingJiraIssueTypes"
:text="checkedIssueType.name || $options.i18n.issueTypeSelect.defaultText"
>
<gl-dropdown-item
......
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import * as Sentry from '@sentry/browser';
import { setHTMLFixture } from 'helpers/fixtures';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockIntegrationProps } from 'jest/integrations/edit/mock_data';
import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
......@@ -11,19 +13,27 @@ import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_field
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
import ResetConfirmationModal from '~/integrations/edit/components/reset_confirmation_modal.vue';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
import waitForPromises from 'helpers/wait_for_promises';
import {
integrationLevels,
TEST_INTEGRATION_EVENT,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
VALIDATE_INTEGRATION_FORM_EVENT,
SAVE_INTEGRATION_EVENT,
I18N_DEFAULT_ERROR_MESSAGE,
} from '~/integrations/constants';
import { createStore } from '~/integrations/edit/store';
import eventHub from '~/integrations/edit/event_hub';
import httpStatus from '~/lib/utils/http_status';
jest.mock('~/integrations/edit/event_hub');
jest.mock('@sentry/browser');
describe('IntegrationForm', () => {
const mockToastShow = jest.fn();
let wrapper;
let dispatch;
let mockAxios;
const createComponent = ({
customStateProps = {},
......@@ -39,6 +49,9 @@ describe('IntegrationForm', () => {
wrapper = shallowMountExtended(IntegrationForm, {
propsData: { ...props, formSelector: '.test' },
provide: {
glFeatures: featureFlags,
},
store,
stubs: {
OverrideDropdown,
......@@ -47,15 +60,21 @@ describe('IntegrationForm', () => {
JiraTriggerFields,
TriggerFields,
},
provide: {
glFeatures: featureFlags,
mocks: {
$toast: {
show: mockToastShow,
},
},
});
};
afterEach(() => {
wrapper.destroy();
});
const createForm = ({ isValid = true } = {}) => {
const mockForm = document.createElement('form');
jest.spyOn(document, 'querySelector').mockReturnValue(mockForm);
jest.spyOn(mockForm, 'checkValidity').mockReturnValue(isValid);
return mockForm;
};
const findOverrideDropdown = () => wrapper.findComponent(OverrideDropdown);
const findActiveCheckbox = () => wrapper.findComponent(ActiveCheckbox);
......@@ -68,6 +87,15 @@ describe('IntegrationForm', () => {
const findJiraIssuesFields = () => wrapper.findComponent(JiraIssuesFields);
const findTriggerFields = () => wrapper.findComponent(TriggerFields);
beforeEach(() => {
mockAxios = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
mockAxios.restore();
});
describe('template', () => {
describe('integrationLevel is instance', () => {
it('renders ConfirmationModal', () => {
......@@ -399,18 +427,9 @@ describe('IntegrationForm', () => {
});
describe('when `test` button is clicked', () => {
let mockForm;
describe.each`
formValid
${true}
${false}
`('when form checkValidity returns $formValid', ({ formValid }) => {
beforeEach(() => {
mockForm = document.createElement('form');
jest.spyOn(document, 'querySelector').mockReturnValue(mockForm);
jest.spyOn(mockForm, 'checkValidity').mockReturnValue(formValid);
describe('when form is invalid', () => {
it('emits `VALIDATE_INTEGRATION_FORM_EVENT` event to the event hub', () => {
createForm({ isValid: false });
createComponent({
customStateProps: {
showActive: true,
......@@ -419,14 +438,70 @@ describe('IntegrationForm', () => {
});
findTestButton().vm.$emit('click', new Event('click'));
expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT);
});
});
describe('when form is valid', () => {
const mockTestPath = '/test';
it('dispatches setIsTesting action', () => {
expect(dispatch).toHaveBeenCalledWith('setIsTesting', true);
beforeEach(() => {
createForm({ isValid: true });
createComponent({
customStateProps: {
showActive: true,
canTest: true,
testPath: mockTestPath,
},
});
});
it(`emits \`TEST_INTEGRATION_EVENT\` event with payload \`${formValid}\``, () => {
expect(eventHub.$emit).toHaveBeenCalledWith(TEST_INTEGRATION_EVENT, formValid);
describe('buttons', () => {
beforeEach(async () => {
await findTestButton().vm.$emit('click', new Event('click'));
});
it('sets test button `loading` prop to `true`', () => {
expect(findTestButton().props('loading')).toBe(true);
});
it('sets save button `disabled` prop to `true`', () => {
expect(findSaveButton().props('disabled')).toBe(true);
});
});
describe.each`
scenario | replyStatus | errorMessage | expectToast | expectSentry
${'when "test settings" request fails'} | ${httpStatus.INTERNAL_SERVER_ERROR} | ${undefined} | ${I18N_DEFAULT_ERROR_MESSAGE} | ${true}
${'when "test settings" returns an error'} | ${httpStatus.OK} | ${'an error'} | ${'an error'} | ${false}
${'when "test settings" succeeds'} | ${httpStatus.OK} | ${undefined} | ${I18N_SUCCESSFUL_CONNECTION_MESSAGE} | ${false}
`('$scenario', ({ replyStatus, errorMessage, expectToast, expectSentry }) => {
beforeEach(async () => {
mockAxios.onPut(mockTestPath).replyOnce(replyStatus, {
error: Boolean(errorMessage),
message: errorMessage,
});
await findTestButton().vm.$emit('click', new Event('click'));
await waitForPromises();
});
it(`calls toast with '${expectToast}'`, () => {
expect(mockToastShow).toHaveBeenCalledWith(expectToast);
});
it('sets `loading` prop of test button to `false`', () => {
expect(findTestButton().props('loading')).toBe(false);
});
it('sets save button `disabled` prop to `false`', () => {
expect(findSaveButton().props('disabled')).toBe(false);
});
it(`${expectSentry ? 'does' : 'does not'} capture exception in Sentry`, () => {
expect(Sentry.captureException).toHaveBeenCalledTimes(expectSentry ? 1 : 0);
});
});
});
});
......
......@@ -5,7 +5,6 @@ import { I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE } from '~/integrations/c
import {
setOverride,
setIsSaving,
setIsTesting,
setIsResetting,
requestResetIntegration,
receiveResetIntegrationSuccess,
......@@ -46,12 +45,6 @@ describe('Integration form store actions', () => {
});
});
describe('setIsTesting', () => {
it('should commit isTesting mutation', () => {
return testAction(setIsTesting, true, state, [{ type: types.SET_IS_TESTING, payload: true }]);
});
});
describe('setIsResetting', () => {
it('should commit isResetting mutation', () => {
return testAction(setIsResetting, true, state, [
......
......@@ -54,20 +54,15 @@ describe('Integration form store getters', () => {
describe('isDisabled', () => {
it.each`
isSaving | isTesting | isResetting | expected
${false} | ${false} | ${false} | ${false}
${true} | ${false} | ${false} | ${true}
${false} | ${true} | ${false} | ${true}
${false} | ${false} | ${true} | ${true}
${false} | ${true} | ${true} | ${true}
${true} | ${false} | ${true} | ${true}
${true} | ${true} | ${false} | ${true}
${true} | ${true} | ${true} | ${true}
isSaving | isResetting | expected
${false} | ${false} | ${false}
${true} | ${false} | ${true}
${false} | ${true} | ${true}
${true} | ${true} | ${true}
`(
'when isSaving = $isSaving, isTesting = $isTesting, isResetting = $isResetting then isDisabled = $expected',
({ isSaving, isTesting, isResetting, expected }) => {
'when isSaving = $isSaving, isResetting = $isResetting then isDisabled = $expected',
({ isSaving, isResetting, expected }) => {
mutations[types.SET_IS_SAVING](state, isSaving);
mutations[types.SET_IS_TESTING](state, isTesting);
mutations[types.SET_IS_RESETTING](state, isResetting);
expect(isDisabled(state)).toBe(expected);
......
......@@ -25,14 +25,6 @@ describe('Integration form store mutations', () => {
});
});
describe(`${types.SET_IS_TESTING}`, () => {
it('sets isTesting', () => {
mutations[types.SET_IS_TESTING](state, true);
expect(state.isTesting).toBe(true);
});
});
describe(`${types.SET_IS_RESETTING}`, () => {
it('sets isResetting', () => {
mutations[types.SET_IS_RESETTING](state, true);
......
......@@ -6,7 +6,6 @@ describe('Integration form state factory', () => {
defaultState: null,
customState: {},
isSaving: false,
isTesting: false,
isResetting: false,
override: false,
isLoadingJiraIssueTypes: false,
......
......@@ -2,13 +2,7 @@ import MockAdaptor from 'axios-mock-adapter';
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
import eventHub from '~/integrations/edit/event_hub';
import axios from '~/lib/utils/axios_utils';
import toast from '~/vue_shared/plugins/global_toast';
import {
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
TEST_INTEGRATION_EVENT,
SAVE_INTEGRATION_EVENT,
} from '~/integrations/constants';
import { SAVE_INTEGRATION_EVENT } from '~/integrations/constants';
import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/vue_shared/plugins/global_toast');
......@@ -29,6 +23,10 @@ describe('IntegrationSettingsForm', () => {
integrationSettingsForm.init();
});
afterEach(() => {
eventHub.dispose(); // clear event hub handlers
});
describe('constructor', () => {
it('should initialize form element refs on class object', () => {
expect(integrationSettingsForm.$form).toBeDefined();
......@@ -47,88 +45,11 @@ describe('IntegrationSettingsForm', () => {
beforeEach(() => {
mockAxios = new MockAdaptor(axios);
jest.spyOn(axios, 'put');
jest.spyOn(integrationSettingsForm, 'testSettings');
jest.spyOn(integrationSettingsForm.$form, 'submit');
});
afterEach(() => {
mockAxios.restore();
eventHub.dispose(); // clear event hub handlers
});
describe('when event hub receives `TEST_INTEGRATION_EVENT`', () => {
describe('when form is valid', () => {
it('should make an ajax request with provided `formData`', async () => {
eventHub.$emit(TEST_INTEGRATION_EVENT, true);
await waitForPromises();
expect(axios.put).toHaveBeenCalledWith(
integrationSettingsForm.testEndPoint,
new FormData(integrationSettingsForm.$form),
);
});
it('should show success message if test is successful', async () => {
jest.spyOn(integrationSettingsForm.$form, 'submit').mockImplementation(() => {});
mockAxios.onPut(integrationSettingsForm.testEndPoint).reply(200, {
error: false,
});
eventHub.$emit(TEST_INTEGRATION_EVENT, true);
await waitForPromises();
expect(toast).toHaveBeenCalledWith(I18N_SUCCESSFUL_CONNECTION_MESSAGE);
});
it('should show error message if ajax request responds with test error', async () => {
const errorMessage = 'Test failed.';
const serviceResponse = 'some error';
mockAxios.onPut(integrationSettingsForm.testEndPoint).reply(200, {
error: true,
message: errorMessage,
service_response: serviceResponse,
test_failed: false,
});
eventHub.$emit(TEST_INTEGRATION_EVENT, true);
await waitForPromises();
expect(toast).toHaveBeenCalledWith(`${errorMessage} ${serviceResponse}`);
});
it('should show error message if ajax request failed', async () => {
mockAxios.onPut(integrationSettingsForm.testEndPoint).networkError();
eventHub.$emit(TEST_INTEGRATION_EVENT, true);
await waitForPromises();
expect(toast).toHaveBeenCalledWith(I18N_DEFAULT_ERROR_MESSAGE);
});
it('should always dispatch `setIsTesting` with `false` once request is completed', async () => {
const dispatchSpy = mockStoreDispatch();
mockAxios.onPut(integrationSettingsForm.testEndPoint).networkError();
eventHub.$emit(TEST_INTEGRATION_EVENT, true);
await waitForPromises();
expect(dispatchSpy).toHaveBeenCalledWith('setIsTesting', false);
});
});
describe('when form is invalid', () => {
it('should dispatch `setIsTesting` with `false` and not call `testSettings`', async () => {
const dispatchSpy = mockStoreDispatch();
eventHub.$emit(TEST_INTEGRATION_EVENT, false);
await waitForPromises();
expect(dispatchSpy).toHaveBeenCalledWith('setIsTesting', false);
expect(integrationSettingsForm.testSettings).not.toHaveBeenCalled();
});
});
});
describe('when event hub receives `SAVE_INTEGRATION_EVENT`', () => {
......@@ -137,7 +58,6 @@ describe('IntegrationSettingsForm', () => {
eventHub.$emit(SAVE_INTEGRATION_EVENT, true);
await waitForPromises();
expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
expect(integrationSettingsForm.$form.submit).toHaveBeenCalledTimes(1);
});
});
......
......@@ -28,7 +28,7 @@ RSpec.shared_context 'project service activation' do
end
def click_test_integration
click_link('Test settings')
click_button('Test settings')
end
def click_test_then_save_integration(expect_test_to_fail: true)
......
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