summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-06-03 09:08:47 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-06-03 09:08:47 +0000
commit31a432e38a8b70d3ffb16afa8d7cfeee4f5f5921 (patch)
treeb59f8b4e2ef7486f13adb01328a749f19b93a023 /spec
parent6b7b853dff4cb7e72d76bd501d75708111c95585 (diff)
downloadgitlab-ce-31a432e38a8b70d3ffb16afa8d7cfeee4f5f5921.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/features/admin/services/admin_activates_prometheus_spec.rb2
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb47
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js20
-rw-r--r--spec/frontend/design_management/components/design_notes/design_reply_form_spec.js7
-rw-r--r--spec/frontend/integrations/edit/components/dynamic_field_spec.js179
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js21
-rw-r--r--spec/frontend/notes/components/discussion_reply_placeholder_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js178
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js274
-rw-r--r--spec/lib/quality/test_level_spec.rb8
-rw-r--r--spec/models/blob_viewer/metrics_dashboard_yml_spec.rb95
-rw-r--r--spec/models/performance_monitoring/prometheus_dashboard_spec.rb119
-rw-r--r--spec/models/performance_monitoring/prometheus_panel_group_spec.rb2
-rw-r--r--spec/models/performance_monitoring/prometheus_panel_spec.rb2
-rw-r--r--spec/serializers/service_field_entity_spec.rb99
15 files changed, 879 insertions, 186 deletions
diff --git a/spec/features/admin/services/admin_activates_prometheus_spec.rb b/spec/features/admin/services/admin_activates_prometheus_spec.rb
index 64c57cd425b..6df1cfb9ae0 100644
--- a/spec/features/admin/services/admin_activates_prometheus_spec.rb
+++ b/spec/features/admin/services/admin_activates_prometheus_spec.rb
@@ -6,6 +6,8 @@ describe 'Admin activates Prometheus' do
let(:admin) { create(:user, :admin) }
before do
+ stub_feature_flags(integration_form_refactor: false)
+
sign_in(admin)
visit(admin_application_settings_services_path)
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 9fc70412975..550dacf7597 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -555,6 +555,53 @@ describe 'File blob', :js do
end
end
+ describe '.gitlab/dashboards/custom-dashboard.yml' do
+ before do
+ project.add_maintainer(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add .gitlab/dashboards/custom-dashboard.yml",
+ file_path: '.gitlab/dashboards/custom-dashboard.yml',
+ file_content: file_content
+ ).execute
+
+ visit_blob('.gitlab/dashboards/custom-dashboard.yml')
+ end
+
+ context 'valid dashboard file' do
+ let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) }
+
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows that dashboard yaml is valid
+ expect(page).to have_content('Metrics Dashboard YAML definition is valid.')
+
+ # shows a learn more link
+ expect(page).to have_link('Learn more')
+ end
+ end
+ end
+
+ context 'invalid dashboard file' do
+ let(:file_content) { "dashboard: 'invalid'" }
+
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows that dashboard yaml is invalid
+ expect(page).to have_content('Metrics Dashboard YAML definition is invalid:')
+ expect(page).to have_content("panel_groups: can't be blank")
+
+ # shows a learn more link
+ expect(page).to have_link('Learn more')
+ end
+ end
+ end
+ end
+
context 'LICENSE' do
before do
visit_blob('LICENSE')
diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
index b16b26ff82f..3b398275f7a 100644
--- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -76,7 +76,7 @@ describe('Design discussions component', () => {
it('hides reply placeholder and opens form on placeholder click', () => {
createComponent();
- findReplyPlaceholder().trigger('click');
+ findReplyPlaceholder().vm.$emit('onMouseDown');
return wrapper.vm.$nextTick().then(() => {
expect(findReplyPlaceholder().exists()).toBe(false);
@@ -130,4 +130,22 @@ describe('Design discussions component', () => {
true,
);
});
+
+ it('closes the form on blur if the form was empty', () => {
+ createComponent({}, { discussionComment: '', isFormRendered: true });
+ findReplyForm().vm.$emit('onBlur');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findReplyForm().exists()).toBe(false);
+ });
+ });
+
+ it('keeps the form open on blur if the form had text', () => {
+ createComponent({}, { discussionComment: 'test', isFormRendered: true });
+ findReplyForm().vm.$emit('onBlur');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findReplyForm().exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
index 34b8f1f9fa8..96e5485b778 100644
--- a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
@@ -39,6 +39,13 @@ describe('Design reply form component', () => {
expect(findTextarea().element).toEqual(document.activeElement);
});
+ it('textarea emits onBlur event on blur', () => {
+ createComponent();
+ findTextarea().trigger('blur');
+
+ expect(wrapper.emitted('onBlur')).toBeTruthy();
+ });
+
it('renders button text as "Comment" when creating a comment', () => {
createComponent();
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
new file mode 100644
index 00000000000..e5710641f81
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -0,0 +1,179 @@
+import { mount } from '@vue/test-utils';
+import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
+import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea } from '@gitlab/ui';
+
+describe('DynamicField', () => {
+ let wrapper;
+
+ const defaultProps = {
+ help: 'The URL of the project',
+ name: 'project_url',
+ placeholder: 'https://jira.example.com',
+ title: 'Project URL',
+ type: 'text',
+ value: '1',
+ };
+
+ const createComponent = props => {
+ wrapper = mount(DynamicField, {
+ propsData: { ...defaultProps, ...props },
+ });
+ };
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ const findGlFormGroup = () => wrapper.find(GlFormGroup);
+ const findGlFormCheckbox = () => wrapper.find(GlFormCheckbox);
+ const findGlFormInput = () => wrapper.find(GlFormInput);
+ const findGlFormSelect = () => wrapper.find(GlFormSelect);
+ const findGlFormTextarea = () => wrapper.find(GlFormTextarea);
+
+ describe('template', () => {
+ describe('dynamic field', () => {
+ describe('type is checkbox', () => {
+ beforeEach(() => {
+ createComponent({
+ type: 'checkbox',
+ });
+ });
+
+ it('renders GlFormCheckbox', () => {
+ expect(findGlFormCheckbox().exists()).toBe(true);
+ });
+
+ it('does not render other types of input', () => {
+ expect(findGlFormSelect().exists()).toBe(false);
+ expect(findGlFormTextarea().exists()).toBe(false);
+ expect(findGlFormInput().exists()).toBe(false);
+ });
+ });
+
+ describe('type is select', () => {
+ beforeEach(() => {
+ createComponent({
+ type: 'select',
+ choices: [['all', 'All details'], ['standard', 'Standard']],
+ });
+ });
+
+ it('renders findGlFormSelect', () => {
+ expect(findGlFormSelect().exists()).toBe(true);
+ expect(findGlFormSelect().findAll('option')).toHaveLength(2);
+ });
+
+ it('does not render other types of input', () => {
+ expect(findGlFormCheckbox().exists()).toBe(false);
+ expect(findGlFormTextarea().exists()).toBe(false);
+ expect(findGlFormInput().exists()).toBe(false);
+ });
+ });
+
+ describe('type is textarea', () => {
+ beforeEach(() => {
+ createComponent({
+ type: 'textarea',
+ });
+ });
+
+ it('renders findGlFormTextarea', () => {
+ expect(findGlFormTextarea().exists()).toBe(true);
+ });
+
+ it('does not render other types of input', () => {
+ expect(findGlFormCheckbox().exists()).toBe(false);
+ expect(findGlFormSelect().exists()).toBe(false);
+ expect(findGlFormInput().exists()).toBe(false);
+ });
+ });
+
+ describe('type is password', () => {
+ beforeEach(() => {
+ createComponent({
+ type: 'password',
+ });
+ });
+
+ it('renders GlFormInput', () => {
+ expect(findGlFormInput().exists()).toBe(true);
+ expect(findGlFormInput().attributes('type')).toBe('password');
+ });
+
+ it('does not render other types of input', () => {
+ expect(findGlFormCheckbox().exists()).toBe(false);
+ expect(findGlFormSelect().exists()).toBe(false);
+ expect(findGlFormTextarea().exists()).toBe(false);
+ });
+ });
+
+ describe('type is text', () => {
+ beforeEach(() => {
+ createComponent({
+ type: 'text',
+ required: true,
+ });
+ });
+
+ it('renders GlFormInput', () => {
+ expect(findGlFormInput().exists()).toBe(true);
+ expect(findGlFormInput().attributes()).toMatchObject({
+ type: 'text',
+ id: 'service_project_url',
+ name: 'service[project_url]',
+ placeholder: defaultProps.placeholder,
+ required: 'required',
+ });
+ });
+
+ it('does not render other types of input', () => {
+ expect(findGlFormCheckbox().exists()).toBe(false);
+ expect(findGlFormSelect().exists()).toBe(false);
+ expect(findGlFormTextarea().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('help text', () => {
+ it('renders description with help text', () => {
+ createComponent();
+
+ expect(
+ findGlFormGroup()
+ .find('small')
+ .text(),
+ ).toBe(defaultProps.help);
+ });
+ });
+
+ describe('label text', () => {
+ it('renders label with title', () => {
+ createComponent();
+
+ expect(
+ findGlFormGroup()
+ .find('label')
+ .text(),
+ ).toBe(defaultProps.title);
+ });
+
+ describe('for password field with some value (hidden by backend)', () => {
+ it('renders label with new password title', () => {
+ createComponent({
+ type: 'password',
+ value: 'true',
+ });
+
+ expect(
+ findGlFormGroup()
+ .find('label')
+ .text(),
+ ).toBe(`Enter new ${defaultProps.title}`);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index c93f63b11d0..b598a71cea8 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -3,6 +3,7 @@ import IntegrationForm from '~/integrations/edit/components/integration_form.vue
import ActiveToggle from '~/integrations/edit/components/active_toggle.vue';
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
+import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
describe('IntegrationForm', () => {
let wrapper;
@@ -95,5 +96,25 @@ describe('IntegrationForm', () => {
expect(findTriggerFields().props('type')).toBe(type);
});
});
+
+ describe('fields is present', () => {
+ it('renders DynamicField for each field', () => {
+ const fields = [
+ { name: 'username', type: 'text' },
+ { name: 'API token', type: 'password' },
+ ];
+
+ createComponent({
+ fields,
+ });
+
+ const dynamicFields = wrapper.findAll(DynamicField);
+
+ expect(dynamicFields).toHaveLength(2);
+ dynamicFields.wrappers.forEach((field, index) => {
+ expect(field.props()).toMatchObject(fields[index]);
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
index a881e44a007..4739dfcfc70 100644
--- a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
+++ b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
@@ -20,7 +20,7 @@ describe('ReplyPlaceholder', () => {
wrapper.destroy();
});
- it('emits onClick even on button click', () => {
+ it('emits onClick event on button click', () => {
findButton().trigger('click');
return wrapper.vm.$nextTick().then(() => {
@@ -30,6 +30,16 @@ describe('ReplyPlaceholder', () => {
});
});
+ it('emits onMouseDown event on button mousedown', () => {
+ findButton().trigger('mousedown');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emitted()).toEqual({
+ onMouseDown: [[]],
+ });
+ });
+ });
+
it('should render reply button', () => {
expect(findButton().text()).toEqual(buttonText);
});
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js
index 98962918b49..e46c63a1a32 100644
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js
+++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js
@@ -1,7 +1,13 @@
-import * as dateTimePickerLib from '~/vue_shared/components/date_time_picker/date_time_picker_lib';
+import timezoneMock from 'timezone-mock';
+
+import {
+ isValidInputString,
+ inputStringToIsoDate,
+ isoDateToInputString,
+} from '~/vue_shared/components/date_time_picker/date_time_picker_lib';
describe('date time picker lib', () => {
- describe('isValidDate', () => {
+ describe('isValidInputString', () => {
[
{
input: '2019-09-09T00:00:00.000Z',
@@ -48,121 +54,137 @@ describe('date time picker lib', () => {
output: false,
},
].forEach(({ input, output }) => {
- it(`isValidDate return ${output} for ${input}`, () => {
- expect(dateTimePickerLib.isValidDate(input)).toBe(output);
+ it(`isValidInputString return ${output} for ${input}`, () => {
+ expect(isValidInputString(input)).toBe(output);
});
});
});
- describe('stringToISODate', () => {
- ['', 'null', undefined, 'abc'].forEach(input => {
+ describe('inputStringToIsoDate', () => {
+ [
+ '',
+ 'null',
+ undefined,
+ 'abc',
+ 'xxxx-xx-xx',
+ '9999-99-19',
+ '2019-19-23',
+ '2019-09-23 x',
+ '2019-09-29 24:24:24',
+ ].forEach(input => {
it(`throws error for invalid input like ${input}`, () => {
- expect(() => dateTimePickerLib.stringToISODate(input)).toThrow();
+ expect(() => inputStringToIsoDate(input)).toThrow();
});
});
+
[
{
- input: '2019-09-09 01:01:01',
- output: '2019-09-09T01:01:01Z',
+ input: '2019-09-08 01:01:01',
+ output: '2019-09-08T01:01:01Z',
},
{
- input: '2019-09-09 00:00:00',
- output: '2019-09-09T00:00:00Z',
+ input: '2019-09-08 00:00:00',
+ output: '2019-09-08T00:00:00Z',
},
{
- input: '2019-09-09 23:59:59',
- output: '2019-09-09T23:59:59Z',
+ input: '2019-09-08 23:59:59',
+ output: '2019-09-08T23:59:59Z',
},
{
- input: '2019-09-09',
- output: '2019-09-09T00:00:00Z',
+ input: '2019-09-08',
+ output: '2019-09-08T00:00:00Z',
},
- ].forEach(({ input, output }) => {
- it(`returns ${output} from ${input}`, () => {
- expect(dateTimePickerLib.stringToISODate(input)).toBe(output);
- });
- });
- });
-
- describe('truncateZerosInDateTime', () => {
- [
{
- input: '',
- output: '',
+ input: '2019-09-08',
+ output: '2019-09-08T00:00:00Z',
},
{
- input: '2019-10-10',
- output: '2019-10-10',
+ input: '2019-09-08 00:00:00',
+ output: '2019-09-08T00:00:00Z',
},
{
- input: '2019-10-10 00:00:01',
- output: '2019-10-10 00:00:01',
+ input: '2019-09-08 23:24:24',
+ output: '2019-09-08T23:24:24Z',
},
{
- input: '2019-10-10 00:00:00',
- output: '2019-10-10',
+ input: '2019-09-08 0:0:0',
+ output: '2019-09-08T00:00:00Z',
},
].forEach(({ input, output }) => {
- it(`truncateZerosInDateTime return ${output} for ${input}`, () => {
- expect(dateTimePickerLib.truncateZerosInDateTime(input)).toBe(output);
+ it(`returns ${output} from ${input}`, () => {
+ expect(inputStringToIsoDate(input)).toBe(output);
});
});
+
+ describe('timezone formatting', () => {
+ const value = '2019-09-08 01:01:01';
+ const utcResult = '2019-09-08T01:01:01Z';
+ const localResult = '2019-09-08T08:01:01Z';
+
+ test.each`
+ val | locatTimezone | utc | result
+ ${value} | ${'UTC'} | ${undefined} | ${utcResult}
+ ${value} | ${'UTC'} | ${false} | ${utcResult}
+ ${value} | ${'UTC'} | ${true} | ${utcResult}
+ ${value} | ${'US/Pacific'} | ${undefined} | ${localResult}
+ ${value} | ${'US/Pacific'} | ${false} | ${localResult}
+ ${value} | ${'US/Pacific'} | ${true} | ${utcResult}
+ `(
+ 'when timezone is $locatTimezone, formats $result for utc = $utc',
+ ({ val, locatTimezone, utc, result }) => {
+ timezoneMock.register(locatTimezone);
+
+ expect(inputStringToIsoDate(val, utc)).toBe(result);
+
+ timezoneMock.unregister();
+ },
+ );
+ });
});
- describe('isDateTimePickerInputValid', () => {
+ describe('isoDateToInputString', () => {
[
{
- input: null,
- output: false,
- },
- {
- input: '',
- output: false,
+ input: '2019-09-08T01:01:01Z',
+ output: '2019-09-08 01:01:01',
},
{
- input: 'xxxx-xx-xx',
- output: false,
+ input: '2019-09-08T01:01:01.999Z',
+ output: '2019-09-08 01:01:01',
},
{
- input: '9999-99-19',
- output: false,
- },
- {
- input: '2019-19-23',
- output: false,
- },
- {
- input: '2019-09-23',
- output: true,
- },
- {
- input: '2019-09-23 x',
- output: false,
- },
- {
- input: '2019-09-29 0:0:0',
- output: false,
- },
- {
- input: '2019-09-29 00:00:00',
- output: true,
- },
- {
- input: '2019-09-29 24:24:24',
- output: false,
- },
- {
- input: '2019-09-29 23:24:24',
- output: true,
- },
- {
- input: '2019-09-29 23:24:24 ',
- output: false,
+ input: '2019-09-08T00:00:00Z',
+ output: '2019-09-08 00:00:00',
},
].forEach(({ input, output }) => {
it(`returns ${output} for ${input}`, () => {
- expect(dateTimePickerLib.isDateTimePickerInputValid(input)).toBe(output);
+ expect(isoDateToInputString(input)).toBe(output);
});
});
+
+ describe('timezone formatting', () => {
+ const value = '2019-09-08T08:01:01Z';
+ const utcResult = '2019-09-08 08:01:01';
+ const localResult = '2019-09-08 01:01:01';
+
+ test.each`
+ val | locatTimezone | utc | result
+ ${value} | ${'UTC'} | ${undefined} | ${utcResult}
+ ${value} | ${'UTC'} | ${false} | ${utcResult}
+ ${value} | ${'UTC'} | ${true} | ${utcResult}
+ ${value} | ${'US/Pacific'} | ${undefined} | ${localResult}
+ ${value} | ${'US/Pacific'} | ${false} | ${localResult}
+ ${value} | ${'US/Pacific'} | ${true} | ${utcResult}
+ `(
+ 'when timezone is $locatTimezone, formats $result for utc = $utc',
+ ({ val, locatTimezone, utc, result }) => {
+ timezoneMock.register(locatTimezone);
+
+ expect(isoDateToInputString(val, utc)).toBe(result);
+
+ timezoneMock.unregister();
+ },
+ );
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
index 90130917d8f..ceea8d2fa92 100644
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
+++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import timezoneMock from 'timezone-mock';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import {
defaultTimeRanges,
@@ -8,16 +9,16 @@ import {
const optionsCount = defaultTimeRanges.length;
describe('DateTimePicker', () => {
- let dateTimePicker;
+ let wrapper;
- const dropdownToggle = () => dateTimePicker.find('.dropdown-toggle');
- const dropdownMenu = () => dateTimePicker.find('.dropdown-menu');
- const applyButtonElement = () => dateTimePicker.find('button.btn-success').element;
- const findQuickRangeItems = () => dateTimePicker.findAll('.dropdown-item');
- const cancelButtonElement = () => dateTimePicker.find('button.btn-secondary').element;
+ const dropdownToggle = () => wrapper.find('.dropdown-toggle');
+ const dropdownMenu = () => wrapper.find('.dropdown-menu');
+ const applyButtonElement = () => wrapper.find('button.btn-success').element;
+ const findQuickRangeItems = () => wrapper.findAll('.dropdown-item');
+ const cancelButtonElement = () => wrapper.find('button.btn-secondary').element;
const createComponent = props => {
- dateTimePicker = mount(DateTimePicker, {
+ wrapper = mount(DateTimePicker, {
propsData: {
...props,
},
@@ -25,54 +26,86 @@ describe('DateTimePicker', () => {
};
afterEach(() => {
- dateTimePicker.destroy();
+ wrapper.destroy();
});
- it('renders dropdown toggle button with selected text', done => {
+ it('renders dropdown toggle button with selected text', () => {
createComponent();
- dateTimePicker.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick(() => {
expect(dropdownToggle().text()).toBe(defaultTimeRange.label);
- done();
+ });
+ });
+
+ it('renders dropdown toggle button with selected text and utc label', () => {
+ createComponent({ utc: true });
+ return wrapper.vm.$nextTick(() => {
+ expect(dropdownToggle().text()).toContain(defaultTimeRange.label);
+ expect(dropdownToggle().text()).toContain('UTC');
});
});
it('renders dropdown with 2 custom time range inputs', () => {
createComponent();
- dateTimePicker.vm.$nextTick(() => {
- expect(dateTimePicker.findAll('input').length).toBe(2);
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.findAll('input').length).toBe(2);
});
});
- it('renders inputs with h/m/s truncated if its all 0s', done => {
- createComponent({
- value: {
+ describe('renders label with h/m/s truncated if possible', () => {
+ [
+ {
+ start: '2019-10-10T00:00:00.000Z',
+ end: '2019-10-10T00:00:00.000Z',
+ label: '2019-10-10 to 2019-10-10',
+ },
+ {
start: '2019-10-10T00:00:00.000Z',
end: '2019-10-14T00:10:00.000Z',
+ label: '2019-10-10 to 2019-10-14 00:10:00',
},
- });
- dateTimePicker.vm.$nextTick(() => {
- expect(dateTimePicker.find('#custom-time-from').element.value).toBe('2019-10-10');
- expect(dateTimePicker.find('#custom-time-to').element.value).toBe('2019-10-14 00:10:00');
- done();
+ {
+ start: '2019-10-10T00:00:00.000Z',
+ end: '2019-10-10T00:00:01.000Z',
+ label: '2019-10-10 to 2019-10-10 00:00:01',
+ },
+ {
+ start: '2019-10-10T00:00:01.000Z',
+ end: '2019-10-10T00:00:01.000Z',
+ label: '2019-10-10 00:00:01 to 2019-10-10 00:00:01',
+ },
+ {
+ start: '2019-10-10T00:00:01.000Z',
+ end: '2019-10-10T00:00:01.000Z',
+ utc: true,
+ label: '2019-10-10 00:00:01 to 2019-10-10 00:00:01 UTC',
+ },
+ ].forEach(({ start, end, utc, label }) => {
+ it(`for start ${start}, end ${end}, and utc ${utc}, label is ${label}`, () => {
+ createComponent({
+ value: { start, end },
+ utc,
+ });
+ return wrapper.vm.$nextTick(() => {
+ expect(dropdownToggle().text()).toBe(label);
+ });
+ });
});
});
- it(`renders dropdown with ${optionsCount} (default) items in quick range`, done => {
+ it(`renders dropdown with ${optionsCount} (default) items in quick range`, () => {
createComponent();
dropdownToggle().trigger('click');
- dateTimePicker.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick(() => {
expect(findQuickRangeItems().length).toBe(optionsCount);
- done();
});
});
- it('renders dropdown with a default quick range item selected', done => {
+ it('renders dropdown with a default quick range item selected', () => {
createComponent();
dropdownToggle().trigger('click');
- dateTimePicker.vm.$nextTick(() => {
- expect(dateTimePicker.find('.dropdown-item.active').exists()).toBe(true);
- expect(dateTimePicker.find('.dropdown-item.active').text()).toBe(defaultTimeRange.label);
- done();
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('.dropdown-item.active').exists()).toBe(true);
+ expect(wrapper.find('.dropdown-item.active').text()).toBe(defaultTimeRange.label);
});
});
@@ -86,78 +119,128 @@ describe('DateTimePicker', () => {
describe('user input', () => {
const fillInputAndBlur = (input, val) => {
- dateTimePicker.find(input).setValue(val);
- return dateTimePicker.vm.$nextTick().then(() => {
- dateTimePicker.find(input).trigger('blur');
- return dateTimePicker.vm.$nextTick();
+ wrapper.find(input).setValue(val);
+ return wrapper.vm.$nextTick().then(() => {
+ wrapper.find(input).trigger('blur');
+ return wrapper.vm.$nextTick();
});
};
- beforeEach(done => {
+ beforeEach(() => {
createComponent();
- dateTimePicker.vm.$nextTick(done);
+ return wrapper.vm.$nextTick();
});
- it('displays inline error message if custom time range inputs are invalid', done => {
- fillInputAndBlur('#custom-time-from', '2019-10-01abc')
+ it('displays inline error message if custom time range inputs are invalid', () => {
+ return fillInputAndBlur('#custom-time-from', '2019-10-01abc')
.then(() => fillInputAndBlur('#custom-time-to', '2019-10-10abc'))
.then(() => {
- expect(dateTimePicker.findAll('.invalid-feedback').length).toBe(2);
- done();
- })
- .catch(done);
+ expect(wrapper.findAll('.invalid-feedback').length).toBe(2);
+ });
});
- it('keeps apply button disabled with invalid custom time range inputs', done => {
- fillInputAndBlur('#custom-time-from', '2019-10-01abc')
+ it('keeps apply button disabled with invalid custom time range inputs', () => {
+ return fillInputAndBlur('#custom-time-from', '2019-10-01abc')
.then(() => fillInputAndBlur('#custom-time-to', '2019-09-19'))
.then(() => {
expect(applyButtonElement().getAttribute('disabled')).toBe('disabled');
- done();
- })
- .catch(done);
+ });
});
- it('enables apply button with valid custom time range inputs', done => {
- fillInputAndBlur('#custom-time-from', '2019-10-01')
+ it('enables apply button with valid custom time range inputs', () => {
+ return fillInputAndBlur('#custom-time-from', '2019-10-01')
.then(() => fillInputAndBlur('#custom-time-to', '2019-10-19'))
.then(() => {
expect(applyButtonElement().getAttribute('disabled')).toBeNull();
- done();
- })
- .catch(done.fail);
+ });
});
- it('emits dates in an object when apply is clicked', done => {
- fillInputAndBlur('#custom-time-from', '2019-10-01')
- .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19'))
- .then(() => {
- applyButtonElement().click();
-
- expect(dateTimePicker.emitted().input).toHaveLength(1);
- expect(dateTimePicker.emitted().input[0]).toEqual([
- {
- end: '2019-10-19T00:00:00Z',
- start: '2019-10-01T00:00:00Z',
- },
- ]);
- done();
- })
- .catch(done.fail);
+ describe('when "apply" is clicked', () => {
+ it('emits iso dates', () => {
+ return fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00')
+ .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19 00:00:00'))
+ .then(() => {
+ applyButtonElement().click();
+
+ expect(wrapper.emitted().input).toHaveLength(1);
+ expect(wrapper.emitted().input[0]).toEqual([
+ {
+ end: '2019-10-19T00:00:00Z',
+ start: '2019-10-01T00:00:00Z',
+ },
+ ]);
+ });
+ });
+
+ it('emits iso dates, for dates without time of day', () => {
+ return fillInputAndBlur('#custom-time-from', '2019-10-01')
+ .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19'))
+ .then(() => {
+ applyButtonElement().click();
+
+ expect(wrapper.emitted().input).toHaveLength(1);
+ expect(wrapper.emitted().input[0]).toEqual([
+ {
+ end: '2019-10-19T00:00:00Z',
+ start: '2019-10-01T00:00:00Z',
+ },
+ ]);
+ });
+ });
+
+ describe('when timezone is different', () => {
+ beforeAll(() => {
+ timezoneMock.register('US/Pacific');
+ });
+ afterAll(() => {
+ timezoneMock.unregister();
+ });
+
+ it('emits iso dates', () => {
+ return fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00')
+ .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19 12:00:00'))
+ .then(() => {
+ applyButtonElement().click();
+
+ expect(wrapper.emitted().input).toHaveLength(1);
+ expect(wrapper.emitted().input[0]).toEqual([
+ {
+ start: '2019-10-01T07:00:00Z',
+ end: '2019-10-19T19:00:00Z',
+ },
+ ]);
+ });
+ });
+
+ it('emits iso dates with utc format', () => {
+ wrapper.setProps({ utc: true });
+ return wrapper.vm
+ .$nextTick()
+ .then(() => fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00'))
+ .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19 12:00:00'))
+ .then(() => {
+ applyButtonElement().click();
+
+ expect(wrapper.emitted().input).toHaveLength(1);
+ expect(wrapper.emitted().input[0]).toEqual([
+ {
+ start: '2019-10-01T00:00:00Z',
+ end: '2019-10-19T12:00:00Z',
+ },
+ ]);
+ });
+ });
+ });
});
- it('unchecks quick range when text is input is clicked', done => {
+ it('unchecks quick range when text is input is clicked', () => {
const findActiveItems = () => findQuickRangeItems().filter(w => w.is('.active'));
expect(findActiveItems().length).toBe(1);
- fillInputAndBlur('#custom-time-from', '2019-10-01')
- .then(() => {
- expect(findActiveItems().length).toBe(0);
-
- done();
- })
- .catch(done.fail);
+ return fillInputAndBlur('#custom-time-from', '2019-10-01').then(() => {
+ expect(findActiveItems().length).toBe(0);
+ });
});
it('emits dates in an object when a is clicked', () => {
@@ -165,23 +248,22 @@ describe('DateTimePicker', () => {
.at(3) // any item
.trigger('click');
- expect(dateTimePicker.emitted().input).toHaveLength(1);
- expect(dateTimePicker.emitted().input[0][0]).toMatchObject({
+ expect(wrapper.emitted().input).toHaveLength(1);
+ expect(wrapper.emitted().input[0][0]).toMatchObject({
duration: {
seconds: expect.any(Number),
},
});
});
- it('hides the popover with cancel button', done => {
+ it('hides the popover with cancel button', () => {
dropdownToggle().trigger('click');
- dateTimePicker.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick(() => {
cancelButtonElement().click();
- dateTimePicker.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick(() => {
expect(dropdownMenu().classes('show')).toBe(false);
- done();
});
});
});
@@ -210,7 +292,7 @@ describe('DateTimePicker', () => {
jest.spyOn(Date, 'now').mockImplementation(() => MOCK_NOW);
});
- it('renders dropdown with a label in the quick range', done => {
+ it('renders dropdown with a label in the quick range', () => {
createComponent({
value: {
duration: { seconds: 60 * 5 },
@@ -218,14 +300,26 @@ describe('DateTimePicker', () => {
options: otherTimeRanges,
});
dropdownToggle().trigger('click');
- dateTimePicker.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick(() => {
expect(dropdownToggle().text()).toBe('5 minutes');
+ });
+ });
- done();
+ it('renders dropdown with a label in the quick range and utc label', () => {
+ createComponent({
+ value: {
+ duration: { seconds: 60 * 5 },
+ },
+ utc: true,
+ options: otherTimeRanges,
+ });
+ dropdownToggle().trigger('click');
+ return wrapper.vm.$nextTick(() => {
+ expect(dropdownToggle().text()).toBe('5 minutes UTC');
});
});
- it('renders dropdown with quick range items', done => {
+ it('renders dropdown with quick range items', () => {
createComponent({
value: {
duration: { seconds: 60 * 2 },
@@ -233,7 +327,7 @@ describe('DateTimePicker', () => {
options: otherTimeRanges,
});
dropdownToggle().trigger('click');
- dateTimePicker.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick(() => {
const items = findQuickRangeItems();
expect(items.length).toBe(Object.keys(otherTimeRanges).length);
@@ -245,22 +339,18 @@ describe('DateTimePicker', () => {
expect(items.at(2).text()).toBe('5 minutes');
expect(items.at(2).is('.active')).toBe(false);
-
- done();
});
});
- it('renders dropdown with a label not in the quick range', done => {
+ it('renders dropdown with a label not in the quick range', () => {
createComponent({
value: {
duration: { seconds: 60 * 4 },
},
});
dropdownToggle().trigger('click');
- dateTimePicker.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick(() => {
expect(dropdownToggle().text()).toBe('2020-01-23 19:56:00 to 2020-01-23 20:00:00');
-
- done();
});
});
});
diff --git a/spec/lib/quality/test_level_spec.rb b/spec/lib/quality/test_level_spec.rb
index b784a92fa85..ad29c80b07a 100644
--- a/spec/lib/quality/test_level_spec.rb
+++ b/spec/lib/quality/test_level_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a pattern' do
expect(subject.pattern(:unit))
- .to eq("spec/{bin,channels,config,db,dependencies,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,serializers,services,sidekiq,support_specs,tasks,uploaders,validators,views,workers,elastic_integration}{,/**/}*_spec.rb")
+ .to eq("spec/{bin,channels,config,db,dependencies,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,serializers,services,sidekiq,support_specs,tasks,uploaders,validators,views,workers,elastic_integration,tooling}{,/**/}*_spec.rb")
end
end
@@ -89,7 +89,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a regexp' do
expect(subject.regexp(:unit))
- .to eq(%r{spec/(bin|channels|config|db|dependencies|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|serializers|services|sidekiq|support_specs|tasks|uploaders|validators|views|workers|elastic_integration)})
+ .to eq(%r{spec/(bin|channels|config|db|dependencies|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|serializers|services|sidekiq|support_specs|tasks|uploaders|validators|views|workers|elastic_integration|tooling)})
end
end
@@ -144,6 +144,10 @@ RSpec.describe Quality::TestLevel do
expect(subject.level_for('spec/models/abuse_report_spec.rb')).to eq(:unit)
end
+ it 'returns the correct level for a tooling test' do
+ expect(subject.level_for('spec/tooling/lib/tooling/test_file_finder_spec.rb')).to eq(:unit)
+ end
+
it 'returns the correct level for a migration test' do
expect(subject.level_for('spec/migrations/add_default_and_free_plans_spec.rb')).to eq(:migration)
end
diff --git a/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb b/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb
new file mode 100644
index 00000000000..7ded9685e7e
--- /dev/null
+++ b/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BlobViewer::MetricsDashboardYml do
+ include FakeBlobHelpers
+ include RepoHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
+ let(:blob) { fake_blob(path: '.gitlab/dashboards/custom-dashboard.yml', data: data) }
+ let(:sha) { sample_commit.id }
+
+ subject(:viewer) { described_class.new(blob) }
+
+ context 'when the definition is valid' do
+ let(:data) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) }
+
+ describe '#valid?' do
+ it 'calls prepare! on the viewer' do
+ allow(PerformanceMonitoring::PrometheusDashboard).to receive(:from_json)
+
+ expect(viewer).to receive(:prepare!)
+
+ viewer.valid?
+ end
+
+ it 'returns true' do
+ expect(PerformanceMonitoring::PrometheusDashboard)
+ .to receive(:from_json).with(YAML.safe_load(data))
+ expect(viewer.valid?).to be_truthy
+ end
+ end
+
+ describe '#errors' do
+ it 'returns nil' do
+ allow(PerformanceMonitoring::PrometheusDashboard).to receive(:from_json)
+
+ expect(viewer.errors).to be nil
+ end
+ end
+ end
+
+ context 'when definition is invalid' do
+ let(:error) { ActiveModel::ValidationError.new(PerformanceMonitoring::PrometheusDashboard.new.tap(&:validate)) }
+ let(:data) do
+ <<~YAML
+ dashboard:
+ YAML
+ end
+
+ describe '#valid?' do
+ it 'returns false' do
+ expect(PerformanceMonitoring::PrometheusDashboard)
+ .to receive(:from_json).and_raise(error)
+
+ expect(viewer.valid?).to be_falsey
+ end
+ end
+
+ describe '#errors' do
+ it 'returns validation errors' do
+ allow(PerformanceMonitoring::PrometheusDashboard)
+ .to receive(:from_json).and_raise(error)
+
+ expect(viewer.errors).to be error.model.errors
+ end
+ end
+ end
+
+ context 'when YAML syntax is invalid' do
+ let(:data) do
+ <<~YAML
+ dashboard: 'empty metrics'
+ panel_groups:
+ - group: 'Group Title'
+ YAML
+ end
+
+ describe '#valid?' do
+ it 'returns false' do
+ expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json)
+ expect(viewer.valid?).to be_falsey
+ end
+ end
+
+ describe '#errors' do
+ it 'returns validation errors' do
+ yaml_wrapped_errors = { 'YAML syntax': ["(<unknown>): did not find expected key while parsing a block mapping at line 1 column 1"] }
+
+ expect(viewer.errors).to be_kind_of ActiveModel::Errors
+ expect(viewer.errors.messages).to eql(yaml_wrapped_errors)
+ end
+ end
+ end
+end
diff --git a/spec/models/performance_monitoring/prometheus_dashboard_spec.rb b/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
index e6fc03a0fb6..4e17b0837c2 100644
--- a/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
+++ b/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
@@ -38,24 +38,123 @@ describe PerformanceMonitoring::PrometheusDashboard do
end
describe 'validations' do
- context 'when dashboard is missing' do
- before do
- json_content['dashboard'] = nil
+ shared_examples 'validation failed' do |errors_messages|
+ it 'raises error with corresponding messages', :aggregate_failures do
+ expect { subject }.to raise_error do |error|
+ expect(error).to be_kind_of(ActiveModel::ValidationError)
+ expect(error.model.errors.messages).to eql(errors_messages)
+ end
end
+ end
- subject { described_class.from_json(json_content) }
+ context 'dashboard definition is missing panels_groups and dashboard keys' do
+ let(:json_content) do
+ {
+ "dashboard" => nil
+ }
+ end
- it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
+ it_behaves_like 'validation failed', panel_groups: ["can't be blank"], dashboard: ["can't be blank"]
end
- context 'when panel groups are missing' do
- before do
- json_content['panel_groups'] = []
+ context 'group definition is missing panels and group keys' do
+ let(:json_content) do
+ {
+ "dashboard" => "Dashboard Title",
+ "templating" => {
+ "variables" => {
+ "variable1" => %w(value1 value2 value3)
+ }
+ },
+ "panel_groups" => [{ "group" => nil }]
+ }
end
- subject { described_class.from_json(json_content) }
+ it_behaves_like 'validation failed', panels: ["can't be blank"], group: ["can't be blank"]
+ end
+
+ context 'panel definition is missing metrics and title keys' do
+ let(:json_content) do
+ {
+ "dashboard" => "Dashboard Title",
+ "templating" => {
+ "variables" => {
+ "variable1" => %w(value1 value2 value3)
+ }
+ },
+ "panel_groups" => [{
+ "group" => "Group Title",
+ "panels" => [{
+ "type" => "area-chart",
+ "y_label" => "Y-Axis"
+ }]
+ }]
+ }
+ end
+
+ it_behaves_like 'validation failed', metrics: ["can't be blank"], title: ["can't be blank"]
+ end
+
+ context 'metrics definition is missing unit, query and query_range keys' do
+ let(:json_content) do
+ {
+ "dashboard" => "Dashboard Title",
+ "templating" => {
+ "variables" => {
+ "variable1" => %w(value1 value2 value3)
+ }
+ },
+ "panel_groups" => [{
+ "group" => "Group Title",
+ "panels" => [{
+ "type" => "area-chart",
+ "title" => "Chart Title",
+ "y_label" => "Y-Axis",
+ "metrics" => [{
+ "id" => "metric_of_ages",
+ "label" => "Metric of Ages",
+ "query_range" => nil
+ }]
+ }]
+ }]
+ }
+ end
+
+ it_behaves_like 'validation failed', unit: ["can't be blank"], query_range: ["can't be blank"], query: ["can't be blank"]
+ end
+
+ # for each parent entry validation first is done to its children,
+ # whole execution is stopped on first encountered error
+ # which is the one that is reported
+ context 'multiple offences on different levels' do
+ let(:json_content) do
+ {
+ "dashboard" => nil,
+ "panel_groups" => [{
+ "group" => nil,
+ "panels" => [{
+ "type" => "area-chart",
+ "title" => nil,
+ "y_label" => "Y-Axis",
+ "metrics" => [{
+ "id" => "metric_of_ages",
+ "label" => "Metric of Ages",
+ "query_range" => 'query'
+ }, {
+ "id" => "metric_of_ages",
+ "unit" => "count",
+ "label" => "Metric of Ages",
+ "query_range" => nil
+ }]
+ }]
+ }, {
+ "group" => 'group',
+ "panels" => nil
+ }]
+ }
+ end
- it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
+ it_behaves_like 'validation failed', unit: ["can't be blank"]
end
end
end
diff --git a/spec/models/performance_monitoring/prometheus_panel_group_spec.rb b/spec/models/performance_monitoring/prometheus_panel_group_spec.rb
index 2447bb5df94..35e02c40c1a 100644
--- a/spec/models/performance_monitoring/prometheus_panel_group_spec.rb
+++ b/spec/models/performance_monitoring/prometheus_panel_group_spec.rb
@@ -32,7 +32,7 @@ describe PerformanceMonitoring::PrometheusPanelGroup do
describe 'validations' do
context 'when group is missing' do
before do
- json_content['group'] = nil
+ json_content.delete('group')
end
subject { described_class.from_json(json_content) }
diff --git a/spec/models/performance_monitoring/prometheus_panel_spec.rb b/spec/models/performance_monitoring/prometheus_panel_spec.rb
index f5e04ec91e2..0f56b4ad8ae 100644
--- a/spec/models/performance_monitoring/prometheus_panel_spec.rb
+++ b/spec/models/performance_monitoring/prometheus_panel_spec.rb
@@ -54,7 +54,7 @@ describe PerformanceMonitoring::PrometheusPanel do
context 'when metrics are missing' do
before do
- json_content['metrics'] = []
+ json_content.delete('metrics')
end
subject { described_class.from_json(json_content) }
diff --git a/spec/serializers/service_field_entity_spec.rb b/spec/serializers/service_field_entity_spec.rb
new file mode 100644
index 00000000000..277890d143a
--- /dev/null
+++ b/spec/serializers/service_field_entity_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ServiceFieldEntity do
+ let(:request) { double('request') }
+
+ subject { described_class.new(field, request: request, service: service).as_json }
+
+ before do
+ allow(request).to receive(:service).and_return(service)
+ end
+
+ describe '#as_json' do
+ context 'Jira Service' do
+ let(:service) { create(:jira_service) }
+
+ context 'field with type text' do
+ let(:field) { service.global_fields.find { |field| field[:name] == 'username' } }
+
+ it 'exposes correct attributes' do
+ expected_hash = {
+ type: 'text',
+ name: 'username',
+ title: 'Username or Email',
+ placeholder: 'Use a username for server version and an email for cloud version',
+ required: true,
+ choices: nil,
+ help: nil,
+ value: 'jira_username'
+ }
+
+ is_expected.to eq(expected_hash)
+ end
+ end
+
+ context 'field with type password' do
+ let(:field) { service.global_fields.find { |field| field[:name] == 'password' } }
+
+ it 'exposes correct attributes but hides password' do
+ expected_hash = {
+ type: 'password',
+ name: 'password',
+ title: 'Password or API token',
+ placeholder: 'Use a password for server version and an API token for cloud version',
+ required: true,
+ choices: nil,
+ help: nil,
+ value: 'true'
+ }
+
+ is_expected.to eq(expected_hash)
+ end
+ end
+ end
+
+ context 'EmailsOnPush Service' do
+ let(:service) { create(:emails_on_push_service) }
+
+ context 'field with type checkbox' do
+ let(:field) { service.global_fields.find { |field| field[:name] == 'send_from_committer_email' } }
+
+ it 'exposes correct attributes' do
+ expected_hash = {
+ type: 'checkbox',
+ name: 'send_from_committer_email',
+ title: 'Send from committer',
+ placeholder: nil,
+ required: nil,
+ choices: nil,
+ value: true
+ }
+
+ is_expected.to include(expected_hash)
+ expect(subject[:help]).to include("Send notifications from the committer's email address if the domain is part of the domain GitLab is running on")
+ end
+ end
+
+ context 'field with type select' do
+ let(:field) { service.global_fields.find { |field| field[:name] == 'branches_to_be_notified' } }
+
+ it 'exposes correct attributes' do
+ expected_hash = {
+ type: 'select',
+ name: 'branches_to_be_notified',
+ title: nil,
+ placeholder: nil,
+ required: nil,
+ choices: [['All branches', 'all'], ['Default branch', 'default'], ['Protected branches', 'protected'], ['Default branch and protected branches', 'default_and_protected']],
+ help: nil,
+ value: nil
+ }
+
+ is_expected.to eq(expected_hash)
+ end
+ end
+ end
+ end
+end