diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-03 09:08:47 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-03 09:08:47 +0000 |
commit | 31a432e38a8b70d3ffb16afa8d7cfeee4f5f5921 (patch) | |
tree | b59f8b4e2ef7486f13adb01328a749f19b93a023 /spec | |
parent | 6b7b853dff4cb7e72d76bd501d75708111c95585 (diff) | |
download | gitlab-ce-31a432e38a8b70d3ffb16afa8d7cfeee4f5f5921.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
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 |