Commit ca65f0aa authored by Tom Quirk's avatar Tom Quirk

Add feature flag for new integrations form

This commit wraps the new, vue-based integration
form element in a feature flag. When disabled,
we render the old, haml-based form element.

The feature flag is disabled by default.
parent 59353a45
...@@ -26,3 +26,5 @@ export const I18N_SUCCESSFUL_CONNECTION_MESSAGE = s__('Integrations|Connection s ...@@ -26,3 +26,5 @@ export const I18N_SUCCESSFUL_CONNECTION_MESSAGE = s__('Integrations|Connection s
export const settingsTabTitle = __('Settings'); export const settingsTabTitle = __('Settings');
export const overridesTabTitle = s__('Integrations|Projects using custom settings'); export const overridesTabTitle = s__('Integrations|Projects using custom settings');
export const INTEGRATION_FORM_SELECTOR = '.js-integration-settings-form';
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE, I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE, I18N_DEFAULT_ERROR_MESSAGE,
I18N_SUCCESSFUL_CONNECTION_MESSAGE, I18N_SUCCESSFUL_CONNECTION_MESSAGE,
INTEGRATION_FORM_SELECTOR,
integrationLevels, integrationLevels,
} from '~/integrations/constants'; } from '~/integrations/constants';
import { refreshCurrentPage } from '~/lib/utils/url_utility'; import { refreshCurrentPage } from '~/lib/utils/url_utility';
...@@ -82,14 +83,34 @@ export default { ...@@ -82,14 +83,34 @@ export default {
disableButtons() { disableButtons() {
return Boolean(this.isSaving || this.isResetting || this.isTesting); return Boolean(this.isSaving || this.isResetting || this.isTesting);
}, },
form() { useVueForm() {
return this.$refs.integrationForm.$el; return this.glFeatures?.vueIntegrationForm;
},
formContainerProps() {
return this.useVueForm
? {
ref: 'integrationForm',
method: 'post',
class: 'gl-mb-3 gl-show-field-errors integration-settings-form',
action: this.propsSource.formPath,
novalidate: !this.integrationActive,
}
: {};
},
formContainer() {
return this.useVueForm ? GlForm : 'div';
}, },
}, },
mounted() {
this.form = this.useVueForm
? this.$refs.integrationForm.$el
: document.querySelector(INTEGRATION_FORM_SELECTOR);
},
methods: { methods: {
...mapActions(['setOverride', 'fetchResetIntegration', 'requestJiraIssueTypes']), ...mapActions(['setOverride', 'fetchResetIntegration', 'requestJiraIssueTypes']),
onSaveClick() { onSaveClick() {
this.isSaving = true; this.isSaving = true;
if (this.integrationActive && !this.form.checkValidity()) { if (this.integrationActive && !this.form.checkValidity()) {
this.isSaving = false; this.isSaving = false;
eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT); eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
...@@ -148,6 +169,16 @@ export default { ...@@ -148,6 +169,16 @@ export default {
}, },
onToggleIntegrationState(integrationActive) { onToggleIntegrationState(integrationActive) {
this.integrationActive = integrationActive; this.integrationActive = integrationActive;
if (!this.form || this.useVueForm) {
return;
}
// If integration will be active, enable form validation.
if (integrationActive) {
this.form.removeAttribute('novalidate');
} else {
this.form.setAttribute('novalidate', true);
}
}, },
}, },
helpHtmlConfig: { helpHtmlConfig: {
...@@ -160,21 +191,17 @@ export default { ...@@ -160,21 +191,17 @@ export default {
</script> </script>
<template> <template>
<gl-form <component :is="formContainer" v-bind="formContainerProps">
ref="integrationForm" <template v-if="useVueForm">
method="post" <input type="hidden" name="_method" value="put" />
class="gl-mb-3 gl-show-field-errors integration-settings-form" <input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
:action="propsSource.formPath" <input
:novalidate="!integrationActive" type="hidden"
> name="redirect_to"
<input type="hidden" name="_method" value="put" /> :value="propsSource.redirectTo"
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" /> data-testid="redirect-to-field"
<input />
type="hidden" </template>
name="redirect_to"
:value="propsSource.redirectTo"
data-testid="redirect-to-field"
/>
<override-dropdown <override-dropdown
v-if="defaultState !== null" v-if="defaultState !== null"
...@@ -284,5 +311,5 @@ export default { ...@@ -284,5 +311,5 @@ export default {
</div> </div>
</div> </div>
</div> </div>
</gl-form> </component>
</template> </template>
...@@ -8,6 +8,9 @@ module Integrations::Actions ...@@ -8,6 +8,9 @@ module Integrations::Actions
include IntegrationsHelper include IntegrationsHelper
before_action :integration, only: [:edit, :update, :overrides, :test] before_action :integration, only: [:edit, :update, :overrides, :test]
before_action do
push_frontend_feature_flag(:vue_integration_form, current_user, default_enabled: :yaml)
end
urgency :low, [:test] urgency :low, [:test]
end end
......
...@@ -12,6 +12,9 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -12,6 +12,9 @@ class Projects::ServicesController < Projects::ApplicationController
before_action :web_hook_logs, only: [:edit, :update] before_action :web_hook_logs, only: [:edit, :update]
before_action :set_deprecation_notice_for_prometheus_integration, only: [:edit, :update] before_action :set_deprecation_notice_for_prometheus_integration, only: [:edit, :update]
before_action :redirect_deprecated_prometheus_integration, only: [:update] before_action :redirect_deprecated_prometheus_integration, only: [:update]
before_action do
push_frontend_feature_flag(:vue_integration_form, current_user, default_enabled: :yaml)
end
respond_to :html respond_to :html
......
...@@ -228,6 +228,10 @@ module IntegrationsHelper ...@@ -228,6 +228,10 @@ module IntegrationsHelper
name: integration.to_param name: integration.to_param
} }
end end
def vue_integration_form_enabled?
Feature.enabled?(:vue_integration_form, current_user, default_enabled: :yaml)
end
end end
IntegrationsHelper.prepend_mod_with('IntegrationsHelper') IntegrationsHelper.prepend_mod_with('IntegrationsHelper')
......
...@@ -6,7 +6,13 @@ ...@@ -6,7 +6,13 @@
- if integration.operating? - if integration.operating?
= sprite_icon('check', css_class: 'gl-text-green-500') = sprite_icon('check', css_class: 'gl-text-green-500')
= render 'shared/integration_settings', integration: integration - if vue_integration_form_enabled?
= render 'shared/integration_settings', integration: integration
- else
= form_for(integration, as: :service, url: scoped_integration_path(integration, project: @project, group: @group), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'test-url' => test_project_integration_path(@project, integration), testid: 'integration-form' } }) do |form|
= render 'shared/integration_settings', form: form, integration: integration
%input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referer }
- if lookup_context.template_exists?('show', "projects/services/#{integration.to_param}", true) - if lookup_context.template_exists?('show', "projects/services/#{integration.to_param}", true)
%hr %hr
= render "projects/services/#{integration.to_param}/show", integration: integration = render "projects/services/#{integration.to_param}/show", integration: integration
- integration = local_assigns.fetch(:integration)
= form_for integration, as: :service, url: scoped_integration_path(integration, group: @group), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'test-url' => scoped_test_integration_path(integration, group: @group), testid: 'integration-form' } } do |form|
= render 'shared/integration_settings', form: form, integration: integration
...@@ -7,4 +7,7 @@ ...@@ -7,4 +7,7 @@
= @integration.title = @integration.title
= render 'shared/integrations/tabs', integration: @integration, active_tab: 'edit' do = render 'shared/integrations/tabs', integration: @integration, active_tab: 'edit' do
= render 'shared/integration_settings', integration: @integration - if vue_integration_form_enabled?
= render 'shared/integration_settings', integration: @integration
- else
= render 'shared/integrations/form', integration: @integration
---
name: vue_integration_form
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77934
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350444
milestone: '14.7'
type: development
group: group::integrations
default_enabled: false
...@@ -19,4 +19,19 @@ RSpec.describe 'User activates the instance-level Mattermost Slash Command integ ...@@ -19,4 +19,19 @@ RSpec.describe 'User activates the instance-level Mattermost Slash Command integ
expect(page).to have_link('Settings', href: edit_path) expect(page).to have_link('Settings', href: edit_path)
expect(page).to have_link('Projects using custom settings', href: overrides_path) expect(page).to have_link('Projects using custom settings', href: overrides_path)
end end
it 'does not render integration form element' do
expect(page).not_to have_selector('[data-testid="integration-form"]')
end
context 'when `vue_integration_form` feature flag is disabled' do
before do
stub_feature_flags(vue_integration_form: false)
visit_instance_integration('Mattermost slash commands')
end
it 'renders integration form element' do
expect(page).to have_selector('[data-testid="integration-form"]')
end
end
end end
...@@ -36,10 +36,16 @@ describe('IntegrationForm', () => { ...@@ -36,10 +36,16 @@ describe('IntegrationForm', () => {
let wrapper; let wrapper;
let dispatch; let dispatch;
let mockAxios; let mockAxios;
let mockForm;
let vueIntegrationFormFeatureFlag;
const createForm = () => {
mockForm = document.createElement('form');
jest.spyOn(document, 'querySelector').mockReturnValue(mockForm);
};
const createComponent = ({ const createComponent = ({
customStateProps = {}, customStateProps = {},
featureFlags = {},
initialState = {}, initialState = {},
props = {}, props = {},
mountFn = shallowMountExtended, mountFn = shallowMountExtended,
...@@ -50,11 +56,12 @@ describe('IntegrationForm', () => { ...@@ -50,11 +56,12 @@ describe('IntegrationForm', () => {
}); });
dispatch = jest.spyOn(store, 'dispatch').mockImplementation(); dispatch = jest.spyOn(store, 'dispatch').mockImplementation();
if (!vueIntegrationFormFeatureFlag) {
createForm();
}
wrapper = mountFn(IntegrationForm, { wrapper = mountFn(IntegrationForm, {
propsData: { ...props, formSelector: '.test' }, propsData: { ...props },
provide: {
glFeatures: featureFlags,
},
store, store,
stubs: { stubs: {
OverrideDropdown, OverrideDropdown,
...@@ -68,6 +75,11 @@ describe('IntegrationForm', () => { ...@@ -68,6 +75,11 @@ describe('IntegrationForm', () => {
show: mockToastShow, show: mockToastShow,
}, },
}, },
provide: {
glFeatures: {
vueIntegrationForm: vueIntegrationFormFeatureFlag,
},
},
}); });
}; };
...@@ -84,6 +96,12 @@ describe('IntegrationForm', () => { ...@@ -84,6 +96,12 @@ describe('IntegrationForm', () => {
const findTriggerFields = () => wrapper.findComponent(TriggerFields); const findTriggerFields = () => wrapper.findComponent(TriggerFields);
const findGlForm = () => wrapper.findComponent(GlForm); const findGlForm = () => wrapper.findComponent(GlForm);
const findRedirectToField = () => wrapper.findByTestId('redirect-to-field'); const findRedirectToField = () => wrapper.findByTestId('redirect-to-field');
const findFormElement = () => (vueIntegrationFormFeatureFlag ? findGlForm().element : mockForm);
const mockFormFunctions = ({ checkValidityReturn }) => {
jest.spyOn(findFormElement(), 'checkValidity').mockReturnValue(checkValidityReturn);
jest.spyOn(findFormElement(), 'submit');
};
beforeEach(() => { beforeEach(() => {
mockAxios = new MockAdapter(axios); mockAxios = new MockAdapter(axios);
...@@ -339,14 +357,17 @@ describe('IntegrationForm', () => { ...@@ -339,14 +357,17 @@ describe('IntegrationForm', () => {
}); });
}); });
it('renders hidden fields', () => { describe('when `vueIntegrationForm` feature flag is $vueIntegrationFormEnabled', () => {
createComponent({ it('renders hidden fields', () => {
customStateProps: { vueIntegrationFormFeatureFlag = true;
redirectTo: '/services', createComponent({
}, customStateProps: {
}); redirectTo: '/services',
},
});
expect(findRedirectToField().attributes('value')).toBe('/services'); expect(findRedirectToField().attributes('value')).toBe('/services');
});
}); });
}); });
...@@ -368,13 +389,17 @@ describe('IntegrationForm', () => { ...@@ -368,13 +389,17 @@ describe('IntegrationForm', () => {
}); });
describe.each` describe.each`
formActive | novalidate formActive | vueIntegrationFormEnabled | novalidate
${true} | ${undefined} ${true} | ${true} | ${null}
${false} | ${'novalidate'} ${false} | ${true} | ${'novalidate'}
${true} | ${false} | ${null}
${false} | ${false} | ${'true'}
`( `(
'when `toggle-integration-active` is emitted with $formActive', 'when `vueIntegrationForm` feature flag is $vueIntegrationFormEnabled and `toggle-integration-active` is emitted with $formActive',
({ formActive, novalidate }) => { ({ formActive, vueIntegrationFormEnabled, novalidate }) => {
beforeEach(async () => { beforeEach(async () => {
vueIntegrationFormFeatureFlag = vueIntegrationFormEnabled;
createComponent({ createComponent({
customStateProps: { customStateProps: {
showActive: true, showActive: true,
...@@ -382,185 +407,198 @@ describe('IntegrationForm', () => { ...@@ -382,185 +407,198 @@ describe('IntegrationForm', () => {
}, },
mountFn: mountExtended, mountFn: mountExtended,
}); });
mockFormFunctions({ checkValidityReturn: false });
await findActiveCheckbox().vm.$emit('toggle-integration-active', formActive); await findActiveCheckbox().vm.$emit('toggle-integration-active', formActive);
}); });
it(`sets noValidate to ${novalidate}`, () => { it(`sets noValidate to ${novalidate}`, () => {
expect(findGlForm().attributes('novalidate')).toBe(novalidate); expect(findFormElement().getAttribute('novalidate')).toBe(novalidate);
}); });
}, },
); );
}); });
describe('when `save` button is clicked', () => { describe.each`
describe('buttons', () => { vueIntegrationFormEnabled
beforeEach(async () => { ${true}
createComponent({ ${false}
customStateProps: { `(
showActive: true, 'when `vueIntegrationForm` feature flag is $vueIntegrationFormEnabled',
canTest: true, ({ vueIntegrationFormEnabled }) => {
initialActivated: true, beforeEach(() => {
}, vueIntegrationFormFeatureFlag = vueIntegrationFormEnabled;
mountFn: mountExtended, });
});
describe('when `save` button is clicked', () => {
await findProjectSaveButton().vm.$emit('click', new Event('click')); describe('buttons', () => {
}); beforeEach(async () => {
createComponent({
it('sets save button `loading` prop to `true`', () => { customStateProps: {
expect(findProjectSaveButton().props('loading')).toBe(true); showActive: true,
}); canTest: true,
initialActivated: true,
it('sets test button `disabled` prop to `true`', () => { },
expect(findTestButton().props('disabled')).toBe(true); mountFn: mountExtended,
}); });
});
await findProjectSaveButton().vm.$emit('click', new Event('click'));
describe.each`
checkValidityReturn | integrationActive
${true} | ${false}
${true} | ${true}
${false} | ${false}
`(
'when form is valid (checkValidity returns $checkValidityReturn and integrationActive is $integrationActive)',
({ integrationActive, checkValidityReturn }) => {
beforeEach(async () => {
createComponent({
customStateProps: {
showActive: true,
canTest: true,
initialActivated: integrationActive,
},
mountFn: mountExtended,
}); });
jest.spyOn(findGlForm().element, 'submit');
jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(checkValidityReturn);
await findProjectSaveButton().vm.$emit('click', new Event('click')); it('sets save button `loading` prop to `true`', () => {
}); expect(findProjectSaveButton().props('loading')).toBe(true);
});
it('submit form', () => { it('sets test button `disabled` prop to `true`', () => {
expect(findGlForm().element.submit).toHaveBeenCalledTimes(1); expect(findTestButton().props('disabled')).toBe(true);
});
}); });
},
);
describe('when form is invalid (checkValidity returns false and integrationActive is true)', () => { describe.each`
beforeEach(async () => { checkValidityReturn | integrationActive
createComponent({ ${true} | ${false}
customStateProps: { ${true} | ${true}
showActive: true, ${false} | ${false}
canTest: true, `(
initialActivated: true, 'when form is valid (checkValidity returns $checkValidityReturn and integrationActive is $integrationActive)',
({ integrationActive, checkValidityReturn }) => {
beforeEach(async () => {
createComponent({
customStateProps: {
showActive: true,
canTest: true,
initialActivated: integrationActive,
},
mountFn: mountExtended,
});
mockFormFunctions({ checkValidityReturn });
await findProjectSaveButton().vm.$emit('click', new Event('click'));
});
it('submits form', () => {
expect(findFormElement().submit).toHaveBeenCalledTimes(1);
});
}, },
mountFn: mountExtended, );
});
jest.spyOn(findGlForm().element, 'submit'); describe('when form is invalid (checkValidity returns false and integrationActive is true)', () => {
jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(false); beforeEach(async () => {
createComponent({
await findProjectSaveButton().vm.$emit('click', new Event('click')); customStateProps: {
}); showActive: true,
canTest: true,
it('does not submit form', () => { initialActivated: true,
expect(findGlForm().element.submit).not.toHaveBeenCalled(); },
}); mountFn: mountExtended,
});
mockFormFunctions({ checkValidityReturn: false });
await findProjectSaveButton().vm.$emit('click', new Event('click'));
});
it('sets save button `loading` prop to `false`', () => { it('does not submit form', () => {
expect(findProjectSaveButton().props('loading')).toBe(false); expect(findFormElement().submit).not.toHaveBeenCalled();
}); });
it('sets test button `disabled` prop to `false`', () => { it('sets save button `loading` prop to `false`', () => {
expect(findTestButton().props('disabled')).toBe(false); expect(findProjectSaveButton().props('loading')).toBe(false);
}); });
it('emits `VALIDATE_INTEGRATION_FORM_EVENT`', () => { it('sets test button `disabled` prop to `false`', () => {
expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT); expect(findTestButton().props('disabled')).toBe(false);
}); });
});
});
describe('when `test` button is clicked', () => { it('emits `VALIDATE_INTEGRATION_FORM_EVENT`', () => {
describe('when form is invalid', () => { expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT);
it('emits `VALIDATE_INTEGRATION_FORM_EVENT` event to the event hub', () => { });
createComponent({
customStateProps: {
showActive: true,
canTest: true,
},
mountFn: mountExtended,
}); });
jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(false);
findTestButton().vm.$emit('click', new Event('click'));
expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT);
}); });
});
describe('when form is valid', () => { describe('when `test` button is clicked', () => {
const mockTestPath = '/test'; describe('when form is invalid', () => {
it('emits `VALIDATE_INTEGRATION_FORM_EVENT` event to the event hub', () => {
createComponent({
customStateProps: {
showActive: true,
canTest: true,
},
mountFn: mountExtended,
});
mockFormFunctions({ checkValidityReturn: false });
beforeEach(() => { findTestButton().vm.$emit('click', new Event('click'));
createComponent({
customStateProps: {
showActive: true,
canTest: true,
testPath: mockTestPath,
},
mountFn: mountExtended,
});
jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(true);
});
describe('buttons', () => { expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT);
beforeEach(async () => { });
await findTestButton().vm.$emit('click', new Event('click'));
}); });
it('sets test button `loading` prop to `true`', () => { describe('when form is valid', () => {
expect(findTestButton().props('loading')).toBe(true); const mockTestPath = '/test';
});
it('sets save button `disabled` prop to `true`', () => { beforeEach(() => {
expect(findProjectSaveButton().props('disabled')).toBe(true); createComponent({
}); customStateProps: {
}); showActive: true,
canTest: true,
describe.each` testPath: mockTestPath,
scenario | replyStatus | errorMessage | expectToast | expectSentry },
${'when "test settings" request fails'} | ${httpStatus.INTERNAL_SERVER_ERROR} | ${undefined} | ${I18N_DEFAULT_ERROR_MESSAGE} | ${true} mountFn: mountExtended,
${'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} mockFormFunctions({ checkValidityReturn: true });
`('$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')); describe('buttons', () => {
await waitForPromises(); beforeEach(async () => {
}); await findTestButton().vm.$emit('click', new Event('click'));
});
it(`calls toast with '${expectToast}'`, () => {
expect(mockToastShow).toHaveBeenCalledWith(expectToast);
});
it('sets `loading` prop of test button to `false`', () => { it('sets test button `loading` prop to `true`', () => {
expect(findTestButton().props('loading')).toBe(false); expect(findTestButton().props('loading')).toBe(true);
}); });
it('sets save button `disabled` prop to `false`', () => { it('sets save button `disabled` prop to `true`', () => {
expect(findProjectSaveButton().props('disabled')).toBe(false); expect(findProjectSaveButton().props('disabled')).toBe(true);
}); });
});
it(`${expectSentry ? 'does' : 'does not'} capture exception in Sentry`, () => { describe.each`
expect(Sentry.captureException).toHaveBeenCalledTimes(expectSentry ? 1 : 0); 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(findProjectSaveButton().props('disabled')).toBe(false);
});
it(`${expectSentry ? 'does' : 'does not'} capture exception in Sentry`, () => {
expect(Sentry.captureException).toHaveBeenCalledTimes(expectSentry ? 1 : 0);
});
});
}); });
}); });
}); },
}); );
describe('when `reset-confirmation-modal` emits `reset` event', () => { describe('when `reset-confirmation-modal` emits `reset` event', () => {
const mockResetPath = '/reset'; const mockResetPath = '/reset';
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'projects/services/_form' do
let(:project) { create(:redmine_project) }
let(:user) { create(:admin) }
before do
assign(:project, project)
allow(controller).to receive(:current_user).and_return(user)
allow(view).to receive_messages(
current_user: user,
can?: true,
current_application_settings: Gitlab::CurrentSettings.current_application_settings,
integration: project.redmine_integration,
request: double(referer: '/services')
)
end
context 'integrations form' do
it 'does not render form element' do
render
expect(rendered).not_to have_selector('[data-testid="integration-form"]')
end
context 'when vue_integration_form feature flag is disabled' do
before do
stub_feature_flags(vue_integration_form: false)
end
it 'renders form element' do
render
expect(rendered).to have_selector('[data-testid="integration-form"]')
end
context 'commit_events and merge_request_events' do
it 'display merge_request_events and commit_events descriptions' do
allow(Integrations::Redmine).to receive(:supported_events).and_return(%w(commit merge_request))
render
expect(rendered).to have_css("input[name='redirect_to'][value='/services']", count: 1, visible: false)
end
end
end
end
end
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