diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-03 09:07:54 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-03 09:07:54 +0000 |
commit | 87ef501eacd66d7166183d20d84e33de022f7002 (patch) | |
tree | fa4e0f41e00a4b6aeb035530be4b5473f51b7a3d /spec | |
parent | f321e51f46bcb628c3e96a44b5ebf3bb1c4033ab (diff) | |
download | gitlab-ce-87ef501eacd66d7166183d20d84e33de022f7002.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
25 files changed, 606 insertions, 236 deletions
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb index 91f3bfcfa40..f0d83bb6bbd 100644 --- a/spec/controllers/profiles_controller_spec.rb +++ b/spec/controllers/profiles_controller_spec.rb @@ -89,6 +89,16 @@ describe ProfilesController, :request_store do expect(user.reload.status.message).to eq('Working hard!') expect(response).to have_gitlab_http_status(:found) end + + it 'allows updating user specified job title' do + title = 'Marketing Executive' + sign_in(user) + + put :update, params: { user: { job_title: title } } + + expect(user.reload.job_title).to eq(title) + expect(response).to have_gitlab_http_status(:found) + end end describe 'PUT update_username' do diff --git a/spec/factories/user_details.rb b/spec/factories/user_details.rb new file mode 100644 index 00000000000..3442f057c44 --- /dev/null +++ b/spec/factories/user_details.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :user_detail do + user + job_title { 'VP of Sales' } + end +end diff --git a/spec/features/error_tracking/user_searches_sentry_errors_spec.rb b/spec/features/error_tracking/user_searches_sentry_errors_spec.rb new file mode 100644 index 00000000000..690c60a3c3f --- /dev/null +++ b/spec/features/error_tracking/user_searches_sentry_errors_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'When a user searches for Sentry errors', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline do + include_context 'sentry error tracking context feature' + + let_it_be(:issues_response_body) { fixture_file('sentry/issues_sample_response.json') } + let_it_be(:error_search_response_body) { fixture_file('sentry/error_list_search_response.json') } + let(:issues_api_url) { "#{sentry_api_urls.issues_url}?limit=20&query=is:unresolved" } + let(:issues_api_url_search) { "#{sentry_api_urls.issues_url}?limit=20&query=is:unresolved%20NotFound" } + + before do + stub_request(:get, issues_api_url).with( + headers: { 'Authorization' => 'Bearer access_token_123' } + ).to_return(status: 200, body: issues_response_body, headers: { 'Content-Type' => 'application/json' }) + + stub_request(:get, issues_api_url_search).with( + headers: { 'Authorization' => 'Bearer access_token_123', 'Content-Type' => 'application/json' } + ).to_return(status: 200, body: error_search_response_body, headers: { 'Content-Type' => 'application/json' }) + end + + it 'displays the results' do + sign_in(project.owner) + visit project_error_tracking_index_path(project) + + page.within(find('.gl-table')) do + results = page.all('.table-row') + expect(results.count).to be(2) + end + + find('.gl-form-input').set('NotFound').native.send_keys(:return) + + page.within(find('.gl-table')) do + results = page.all('.table-row') + expect(results.count).to be(1) + expect(results.first).to have_content('NotFound') + end + end +end diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb index a37fc120b86..5cc61333bb4 100644 --- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb +++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb @@ -64,6 +64,10 @@ describe 'Merge request > User merges when pipeline succeeds', :js do before do click_button "Merge when pipeline succeeds" click_link "Cancel automatic merge" + + wait_for_requests + + expect(page).to have_content 'Merge when pipeline succeeds', wait: 0 end it_behaves_like 'Merge when pipeline succeeds activator' diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb index 23358d5cd67..87e467571e6 100644 --- a/spec/features/projects/settings/pipelines_settings_spec.rb +++ b/spec/features/projects/settings/pipelines_settings_spec.rb @@ -61,6 +61,28 @@ describe "Projects > Settings > Pipelines settings" do expect(checkbox).to be_checked end + it 'updates forward_deployment_enabled' do + visit project_settings_ci_cd_path(project) + + checkbox = find_field('project_forward_deployment_enabled') + expect(checkbox).to be_checked + + checkbox.set(false) + + page.within '#js-general-pipeline-settings' do + click_on 'Save changes' + end + + expect(page.status_code).to eq(200) + + page.within '#js-general-pipeline-settings' do + expect(page).to have_button('Save changes', disabled: false) + end + + checkbox = find_field('project_forward_deployment_enabled') + expect(checkbox).not_to be_checked + end + describe 'Auto DevOps' do context 'when auto devops is turned on instance-wide' do before do diff --git a/spec/fixtures/sentry/error_list_search_response.json b/spec/fixtures/sentry/error_list_search_response.json new file mode 100644 index 00000000000..e77c837b48c --- /dev/null +++ b/spec/fixtures/sentry/error_list_search_response.json @@ -0,0 +1,42 @@ +[{ + "lastSeen": "2018-12-31T12:00:11Z", + "numComments": 0, + "userCount": 0, + "stats": { + "24h": [ + [ + 1546437600, + 0 + ] + ] + }, + "culprit": "sentry.tasks.reports.deliver_organization_user_report", + "title": "NotFound desc = GetRepoPath: not a git repository", + "id": "13", + "assignedTo": null, + "logger": null, + "type": "error", + "annotations": [], + "metadata": { + "type": "gaierror", + "value": "[Errno -2] Name or service not known" + }, + "status": "unresolved", + "subscriptionDetails": null, + "isPublic": false, + "hasSeen": false, + "shortId": "INTERNAL-4", + "shareId": null, + "firstSeen": "2018-12-17T12:00:14Z", + "count": "17283712", + "permalink": "35.228.54.90/sentry/internal/issues/13/", + "level": "error", + "isSubscribed": true, + "isBookmarked": false, + "project": { + "slug": "internal", + "id": "1", + "name": "Internal" + }, + "statusDetails": {} +}] diff --git a/spec/fixtures/sentry/issues_sample_response.json b/spec/fixtures/sentry/issues_sample_response.json index ed22499cfa1..495562ac960 100644 --- a/spec/fixtures/sentry/issues_sample_response.json +++ b/spec/fixtures/sentry/issues_sample_response.json @@ -1,4 +1,5 @@ -[{ +[ + { "lastSeen": "2018-12-31T12:00:11Z", "numComments": 0, "userCount": 0, @@ -39,4 +40,47 @@ "name": "Internal" }, "statusDetails": {} - }] + }, + { + "lastSeen": "2018-12-31T12:00:11Z", + "numComments": 0, + "userCount": 0, + "stats": { + "24h": [ + [ + 1546437600, + 0 + ] + ] + }, + "culprit": "sentry.tasks.reports.deliver_organization_user_report", + "title": "NotFound desc = GetRepoPath: not a git repository", + "id": "13", + "assignedTo": null, + "logger": null, + "type": "error", + "annotations": [], + "metadata": { + "type": "gaierror", + "value": "GetRepoPath: not a git repository" + }, + "status": "unresolved", + "subscriptionDetails": null, + "isPublic": false, + "hasSeen": false, + "shortId": "INTERNAL-4", + "shareId": null, + "firstSeen": "2018-12-17T12:00:14Z", + "count": "17283712", + "permalink": "35.228.54.90/sentry/internal/issues/13/", + "level": "error", + "isSubscribed": true, + "isBookmarked": false, + "project": { + "slug": "internal", + "id": "1", + "name": "Internal" + }, + "statusDetails": {} + } +] diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js new file mode 100644 index 00000000000..7723af07d8c --- /dev/null +++ b/spec/frontend/boards/components/boards_selector_spec.js @@ -0,0 +1,155 @@ +import Vue from 'vue'; +import { mount } from '@vue/test-utils'; +import { GlDropdown } from '@gitlab/ui'; +import { TEST_HOST } from 'spec/test_constants'; +import BoardsSelector from '~/boards/components/boards_selector.vue'; +import boardsStore from '~/boards/stores/boards_store'; + +const throttleDuration = 1; + +function boardGenerator(n) { + return new Array(n).fill().map((board, id) => { + const name = `board${id}`; + + return { + id, + name, + }; + }); +} + +describe('BoardsSelector', () => { + let wrapper; + let allBoardsResponse; + let recentBoardsResponse; + const boards = boardGenerator(20); + const recentBoards = boardGenerator(5); + + const fillSearchBox = filterTerm => { + const searchBox = wrapper.find({ ref: 'searchBox' }); + const searchBoxInput = searchBox.find('input'); + searchBoxInput.setValue(filterTerm); + searchBoxInput.trigger('input'); + }; + + const getDropdownItems = () => wrapper.findAll('.js-dropdown-item'); + const getDropdownHeaders = () => wrapper.findAll('.dropdown-bold-header'); + + beforeEach(() => { + boardsStore.setEndpoints({ + boardsEndpoint: '', + recentBoardsEndpoint: '', + listsEndpoint: '', + bulkUpdatePath: '', + boardId: '', + }); + + allBoardsResponse = Promise.resolve({ + data: boards, + }); + recentBoardsResponse = Promise.resolve({ + data: recentBoards, + }); + + boardsStore.allBoards = jest.fn(() => allBoardsResponse); + boardsStore.recentBoards = jest.fn(() => recentBoardsResponse); + + const Component = Vue.extend(BoardsSelector); + wrapper = mount(Component, { + propsData: { + throttleDuration, + currentBoard: { + id: 1, + name: 'Development', + milestone_id: null, + weight: null, + assignee_id: null, + labels: [], + }, + milestonePath: `${TEST_HOST}/milestone/path`, + boardBaseUrl: `${TEST_HOST}/board/base/url`, + hasMissingBoards: false, + canAdminBoard: true, + multipleIssueBoardsAvailable: true, + labelsPath: `${TEST_HOST}/labels/path`, + projectId: 42, + groupId: 19, + scopedIssueBoardFeatureEnabled: true, + weights: [], + }, + attachToDocument: true, + }); + + // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time + wrapper.find(GlDropdown).vm.$emit('show'); + + return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => Vue.nextTick()); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('filtering', () => { + it('shows all boards without filtering', () => { + expect(getDropdownItems().length).toBe(boards.length + recentBoards.length); + }); + + it('shows only matching boards when filtering', () => { + const filterTerm = 'board1'; + const expectedCount = boards.filter(board => board.name.includes(filterTerm)).length; + + fillSearchBox(filterTerm); + + return Vue.nextTick().then(() => { + expect(getDropdownItems().length).toBe(expectedCount); + }); + }); + + it('shows message if there are no matching boards', () => { + fillSearchBox('does not exist'); + + return Vue.nextTick().then(() => { + expect(getDropdownItems().length).toBe(0); + expect(wrapper.text().includes('No matching boards found')).toBe(true); + }); + }); + }); + + describe('recent boards section', () => { + it('shows only when boards are greater than 10', () => { + const expectedCount = 2; // Recent + All + + expect(getDropdownHeaders().length).toBe(expectedCount); + }); + + it('does not show when boards are less than 10', () => { + wrapper.setData({ + boards: boards.slice(0, 5), + }); + + return Vue.nextTick().then(() => { + expect(getDropdownHeaders().length).toBe(0); + }); + }); + + it('does not show when recentBoards api returns empty array', () => { + wrapper.setData({ + recentBoards: [], + }); + + return Vue.nextTick().then(() => { + expect(getDropdownHeaders().length).toBe(0); + }); + }); + + it('does not show when search is active', () => { + fillSearchBox('Random string'); + + return Vue.nextTick().then(() => { + expect(getDropdownHeaders().length).toBe(0); + }); + }); + }); +}); diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js index 94bf0189c91..e43f9569ffc 100644 --- a/spec/frontend/error_tracking/components/error_details_spec.js +++ b/spec/frontend/error_tracking/components/error_details_spec.js @@ -1,8 +1,15 @@ import { createLocalVue, shallowMount } from '@vue/test-utils'; import Vuex from 'vuex'; import { __ } from '~/locale'; -import { GlLoadingIcon, GlLink, GlBadge, GlFormInput, GlAlert, GlSprintf } from '@gitlab/ui'; -import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import { + GlButton, + GlLoadingIcon, + GlLink, + GlBadge, + GlFormInput, + GlAlert, + GlSprintf, +} from '@gitlab/ui'; import Stacktrace from '~/error_tracking/components/stacktrace.vue'; import ErrorDetails from '~/error_tracking/components/error_details.vue'; import { @@ -28,7 +35,7 @@ describe('ErrorDetails', () => { function mountComponent() { wrapper = shallowMount(ErrorDetails, { - stubs: { LoadingButton, GlSprintf }, + stubs: { GlButton, GlSprintf }, localVue, store, mocks, @@ -127,7 +134,7 @@ describe('ErrorDetails', () => { expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); expect(wrapper.find(Stacktrace).exists()).toBe(false); expect(wrapper.find(GlBadge).exists()).toBe(false); - expect(wrapper.findAll('button').length).toBe(3); + expect(wrapper.findAll(GlButton).length).toBe(3); }); describe('Badges', () => { @@ -226,7 +233,7 @@ describe('ErrorDetails', () => { it('should submit the form', () => { window.HTMLFormElement.prototype.submit = () => {}; const submitSpy = jest.spyOn(wrapper.vm.$refs.sentryIssueForm, 'submit'); - wrapper.find('[data-qa-selector="create_issue_button"]').trigger('click'); + wrapper.find('[data-qa-selector="create_issue_button"]').vm.$emit('click'); expect(submitSpy).toHaveBeenCalled(); submitSpy.mockRestore(); }); @@ -255,14 +262,14 @@ describe('ErrorDetails', () => { }); it('marks error as ignored when ignore button is clicked', () => { - findUpdateIgnoreStatusButton().trigger('click'); + findUpdateIgnoreStatusButton().vm.$emit('click'); expect(actions.updateIgnoreStatus.mock.calls[0][1]).toEqual( expect.objectContaining({ status: errorStatus.IGNORED }), ); }); it('marks error as resolved when resolve button is clicked', () => { - findUpdateResolveStatusButton().trigger('click'); + findUpdateResolveStatusButton().vm.$emit('click'); expect(actions.updateResolveStatus.mock.calls[0][1]).toEqual( expect.objectContaining({ status: errorStatus.RESOLVED }), ); @@ -281,14 +288,14 @@ describe('ErrorDetails', () => { }); it('marks error as unresolved when ignore button is clicked', () => { - findUpdateIgnoreStatusButton().trigger('click'); + findUpdateIgnoreStatusButton().vm.$emit('click'); expect(actions.updateIgnoreStatus.mock.calls[0][1]).toEqual( expect.objectContaining({ status: errorStatus.UNRESOLVED }), ); }); it('marks error as resolved when resolve button is clicked', () => { - findUpdateResolveStatusButton().trigger('click'); + findUpdateResolveStatusButton().vm.$emit('click'); expect(actions.updateResolveStatus.mock.calls[0][1]).toEqual( expect.objectContaining({ status: errorStatus.RESOLVED }), ); @@ -307,14 +314,14 @@ describe('ErrorDetails', () => { }); it('marks error as ignored when ignore button is clicked', () => { - findUpdateIgnoreStatusButton().trigger('click'); + findUpdateIgnoreStatusButton().vm.$emit('click'); expect(actions.updateIgnoreStatus.mock.calls[0][1]).toEqual( expect.objectContaining({ status: errorStatus.IGNORED }), ); }); it('marks error as unresolved when unresolve button is clicked', () => { - findUpdateResolveStatusButton().trigger('click'); + findUpdateResolveStatusButton().vm.$emit('click'); expect(actions.updateResolveStatus.mock.calls[0][1]).toEqual( expect.objectContaining({ status: errorStatus.UNRESOLVED }), ); diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js index b632b461eb9..f852a3091aa 100644 --- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js +++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js @@ -42,9 +42,6 @@ describe('ErrorTrackingList', () => { ...stubChildren(ErrorTrackingList), ...stubs, }, - data() { - return { errorSearchQuery: 'search' }; - }, }); } @@ -164,8 +161,9 @@ describe('ErrorTrackingList', () => { }); it('it searches by query', () => { + findSearchBox().vm.$emit('input', 'search'); findSearchBox().trigger('keyup.enter'); - expect(actions.searchByQuery.mock.calls[0][1]).toEqual(wrapper.vm.errorSearchQuery); + expect(actions.searchByQuery.mock.calls[0][1]).toBe('search'); }); it('it sorts by fields', () => { diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap index c705270343b..77f7f2e0609 100644 --- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap +++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap @@ -72,7 +72,7 @@ exports[`Dashboard template matches the default snapshot 1`] = ` </gl-form-group-stub> <gl-form-group-stub - class="col-sm-6 col-md-6 col-lg-4" + class="col-sm-auto col-md-auto col-lg-auto" label="Show last" label-for="monitor-time-window-dropdown" label-size="sm" @@ -83,6 +83,21 @@ exports[`Dashboard template matches the default snapshot 1`] = ` /> </gl-form-group-stub> + <gl-form-group-stub + class="col-sm-2 col-md-2 col-lg-1 refresh-dashboard-button" + > + <gl-button-stub + size="md" + title="Reload this page" + variant="default" + > + <icon-stub + name="repeat" + size="16" + /> + </gl-button-stub> + </gl-form-group-stub> + <!----> </div> </div> diff --git a/spec/frontend/monitoring/components/charts/options_spec.js b/spec/frontend/monitoring/components/charts/options_spec.js new file mode 100644 index 00000000000..d219a6627bf --- /dev/null +++ b/spec/frontend/monitoring/components/charts/options_spec.js @@ -0,0 +1,60 @@ +import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format'; +import { getYAxisOptions, getTooltipFormatter } from '~/monitoring/components/charts/options'; + +describe('options spec', () => { + describe('getYAxisOptions', () => { + it('default options', () => { + const options = getYAxisOptions(); + + expect(options).toMatchObject({ + name: expect.any(String), + axisLabel: { + formatter: expect.any(Function), + }, + scale: true, + boundaryGap: [expect.any(Number), expect.any(Number)], + }); + + expect(options.name).not.toHaveLength(0); + }); + + it('name options', () => { + const yAxisName = 'My axis values'; + const options = getYAxisOptions({ + name: yAxisName, + }); + + expect(options).toMatchObject({ + name: yAxisName, + nameLocation: 'center', + nameGap: expect.any(Number), + }); + }); + + it('formatter options', () => { + const options = getYAxisOptions({ + format: SUPPORTED_FORMATS.bytes, + }); + + expect(options.axisLabel.formatter).toEqual(expect.any(Function)); + expect(options.axisLabel.formatter(1)).toBe('1.00B'); + }); + }); + + describe('getTooltipFormatter', () => { + it('default format', () => { + const formatter = getTooltipFormatter(); + + expect(formatter).toEqual(expect.any(Function)); + expect(formatter(1)).toBe('1.000'); + }); + + it('defined format', () => { + const formatter = getTooltipFormatter({ + format: SUPPORTED_FORMATS.bytes, + }); + + expect(formatter(1)).toBe('1.000B'); + }); + }); +}); diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js index e9322d6b5a9..8dcb54e3fd9 100644 --- a/spec/frontend/monitoring/components/charts/time_series_spec.js +++ b/spec/frontend/monitoring/components/charts/time_series_spec.js @@ -190,7 +190,8 @@ describe('Time series component', () => { it('formats tooltip content', () => { const name = 'Total'; - const value = '5.556'; + const value = '5.556MB'; + const dataIndex = 0; const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel); @@ -348,9 +349,9 @@ describe('Time series component', () => { }); }); - it('additional y axis data', () => { + it('additional y-axis data', () => { const mockCustomYAxisOption = { - name: 'Custom y axis label', + name: 'Custom y-axis label', axisLabel: { formatter: jest.fn(), }, @@ -397,8 +398,8 @@ describe('Time series component', () => { deploymentFormatter = getChartOptions().yAxis[1].axisLabel.formatter; }); - it('rounds to 3 decimal places', () => { - expect(dataFormatter(0.88888)).toBe('0.889'); + it('formats and rounds to 2 decimal places', () => { + expect(dataFormatter(0.88888)).toBe('0.89MB'); }); it('deployment formatter is set as is required to display a tooltip', () => { @@ -421,7 +422,7 @@ describe('Time series component', () => { }); describe('yAxisLabel', () => { - it('y axis is configured correctly', () => { + it('y-axis is configured correctly', () => { const { yAxis } = getChartOptions(); expect(yAxis).toHaveLength(2); diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js index 6f05207204e..bec22b28a5c 100644 --- a/spec/frontend/monitoring/components/dashboard_spec.js +++ b/spec/frontend/monitoring/components/dashboard_spec.js @@ -214,6 +214,19 @@ describe('Dashboard', () => { }); }); + it('renders the refresh dashboard button', () => { + createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] }); + + setupComponentStore(wrapper); + + return wrapper.vm.$nextTick().then(() => { + const refreshBtn = wrapper.findAll({ ref: 'refreshDashboardBtn' }); + + expect(refreshBtn).toHaveLength(1); + expect(refreshBtn.is(GlButton)).toBe(true); + }); + }); + describe('when one of the metrics is missing', () => { beforeEach(() => { createShallowWrapper({ hasMetrics: true }); diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js index 32daf990ad3..60b1510973d 100644 --- a/spec/frontend/monitoring/mock_data.js +++ b/spec/frontend/monitoring/mock_data.js @@ -393,13 +393,16 @@ export const metricsDashboardPayload = { type: 'area-chart', y_label: 'Total Memory Used', weight: 4, + y_axis: { + format: 'megabytes', + }, metrics: [ { id: 'system_metrics_kubernetes_container_memory_total', query_range: - 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024', + 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1000/1000', label: 'Total', - unit: 'GB', + unit: 'MB', metric_id: 12, prometheus_endpoint_path: 'http://test', }, diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js index 57418e90470..2bd8af9b7d5 100644 --- a/spec/frontend/monitoring/store/utils_spec.js +++ b/spec/frontend/monitoring/store/utils_spec.js @@ -1,3 +1,4 @@ +import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format'; import { uniqMetricsId, parseEnvironmentsResponse, @@ -44,6 +45,11 @@ describe('mapToDashboardViewModel', () => { title: 'Title A', type: 'chart-type', y_label: 'Y Label A', + yAxis: { + name: 'Y Label A', + format: 'number', + precision: 2, + }, metrics: [], }, ], @@ -90,6 +96,98 @@ describe('mapToDashboardViewModel', () => { }); }); + describe('panel mapping', () => { + const panelTitle = 'Panel Title'; + const yAxisName = 'Y Axis Name'; + + let dashboard; + + const setupWithPanel = panel => { + dashboard = { + panel_groups: [ + { + panels: [panel], + }, + ], + }; + }; + + const getMappedPanel = () => mapToDashboardViewModel(dashboard).panelGroups[0].panels[0]; + + it('group y_axis defaults', () => { + setupWithPanel({ + title: panelTitle, + }); + + expect(getMappedPanel()).toEqual({ + title: panelTitle, + y_label: '', + yAxis: { + name: '', + format: SUPPORTED_FORMATS.number, + precision: 2, + }, + metrics: [], + }); + }); + + it('panel with y_axis.name', () => { + setupWithPanel({ + y_axis: { + name: yAxisName, + }, + }); + + expect(getMappedPanel().y_label).toBe(yAxisName); + expect(getMappedPanel().yAxis.name).toBe(yAxisName); + }); + + it('panel with y_axis.name and y_label, displays y_axis.name', () => { + setupWithPanel({ + y_label: 'Ignored Y Label', + y_axis: { + name: yAxisName, + }, + }); + + expect(getMappedPanel().y_label).toBe(yAxisName); + expect(getMappedPanel().yAxis.name).toBe(yAxisName); + }); + + it('group y_label', () => { + setupWithPanel({ + y_label: yAxisName, + }); + + expect(getMappedPanel().y_label).toBe(yAxisName); + expect(getMappedPanel().yAxis.name).toBe(yAxisName); + }); + + it('group y_axis format and precision', () => { + setupWithPanel({ + title: panelTitle, + y_axis: { + precision: 0, + format: SUPPORTED_FORMATS.bytes, + }, + }); + + expect(getMappedPanel().yAxis.format).toBe(SUPPORTED_FORMATS.bytes); + expect(getMappedPanel().yAxis.precision).toBe(0); + }); + + it('group y_axis unsupported format defaults to number', () => { + setupWithPanel({ + title: panelTitle, + y_axis: { + format: 'invalid_format', + }, + }); + + expect(getMappedPanel().yAxis.format).toBe(SUPPORTED_FORMATS.number); + }); + }); + describe('metrics mapping', () => { const defaultLabel = 'Panel Label'; const dashboardWithMetric = (metric, label = defaultLabel) => ({ diff --git a/spec/javascripts/boards/components/boards_selector_spec.js b/spec/javascripts/boards/components/boards_selector_spec.js deleted file mode 100644 index 16ec3b801cd..00000000000 --- a/spec/javascripts/boards/components/boards_selector_spec.js +++ /dev/null @@ -1,203 +0,0 @@ -import Vue from 'vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import { TEST_HOST } from 'spec/test_constants'; -import BoardsSelector from '~/boards/components/boards_selector.vue'; -import boardsStore from '~/boards/stores/boards_store'; - -const throttleDuration = 1; - -function boardGenerator(n) { - return new Array(n).fill().map((board, id) => { - const name = `board${id}`; - - return { - id, - name, - }; - }); -} - -describe('BoardsSelector', () => { - let vm; - let allBoardsResponse; - let recentBoardsResponse; - let fillSearchBox; - const boards = boardGenerator(20); - const recentBoards = boardGenerator(5); - - beforeEach(done => { - setFixtures('<div class="js-boards-selector"></div>'); - window.gl = window.gl || {}; - - boardsStore.setEndpoints({ - boardsEndpoint: '', - recentBoardsEndpoint: '', - listsEndpoint: '', - bulkUpdatePath: '', - boardId: '', - }); - - allBoardsResponse = Promise.resolve({ - data: boards, - }); - recentBoardsResponse = Promise.resolve({ - data: recentBoards, - }); - - spyOn(boardsStore, 'allBoards').and.returnValue(allBoardsResponse); - spyOn(boardsStore, 'recentBoards').and.returnValue(recentBoardsResponse); - - const Component = Vue.extend(BoardsSelector); - vm = mountComponent( - Component, - { - throttleDuration, - currentBoard: { - id: 1, - name: 'Development', - milestone_id: null, - weight: null, - assignee_id: null, - labels: [], - }, - milestonePath: `${TEST_HOST}/milestone/path`, - boardBaseUrl: `${TEST_HOST}/board/base/url`, - hasMissingBoards: false, - canAdminBoard: true, - multipleIssueBoardsAvailable: true, - labelsPath: `${TEST_HOST}/labels/path`, - projectId: 42, - groupId: 19, - scopedIssueBoardFeatureEnabled: true, - weights: [], - }, - document.querySelector('.js-boards-selector'), - ); - - // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time - vm.$children[0].$emit('show'); - - Promise.all([allBoardsResponse, recentBoardsResponse]) - .then(() => vm.$nextTick()) - .then(done) - .catch(done.fail); - - fillSearchBox = filterTerm => { - const { searchBox } = vm.$refs; - const searchBoxInput = searchBox.$el.querySelector('input'); - searchBoxInput.value = filterTerm; - searchBoxInput.dispatchEvent(new Event('input')); - }; - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('filtering', () => { - it('shows all boards without filtering', done => { - vm.$nextTick() - .then(() => { - const dropdownItem = vm.$el.querySelectorAll('.js-dropdown-item'); - - expect(dropdownItem.length).toBe(boards.length + recentBoards.length); - }) - .then(done) - .catch(done.fail); - }); - - it('shows only matching boards when filtering', done => { - const filterTerm = 'board1'; - const expectedCount = boards.filter(board => board.name.includes(filterTerm)).length; - - fillSearchBox(filterTerm); - - vm.$nextTick() - .then(() => { - const dropdownItems = vm.$el.querySelectorAll('.js-dropdown-item'); - - expect(dropdownItems.length).toBe(expectedCount); - }) - .then(done) - .catch(done.fail); - }); - - it('shows message if there are no matching boards', done => { - fillSearchBox('does not exist'); - - vm.$nextTick() - .then(() => { - const dropdownItems = vm.$el.querySelectorAll('.js-dropdown-item'); - - expect(dropdownItems.length).toBe(0); - expect(vm.$el).toContainText('No matching boards found'); - }) - .then(done) - .catch(done.fail); - }); - }); - - describe('recent boards section', () => { - it('shows only when boards are greater than 10', done => { - vm.$nextTick() - .then(() => { - const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); - - const expectedCount = 2; // Recent + All - - expect(expectedCount).toBe(headerEls.length); - }) - .then(done) - .catch(done.fail); - }); - - it('does not show when boards are less than 10', done => { - spyOn(vm, 'initScrollFade'); - spyOn(vm, 'setScrollFade'); - - vm.$nextTick() - .then(() => { - vm.boards = vm.boards.slice(0, 5); - }) - .then(vm.$nextTick) - .then(() => { - const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); - const expectedCount = 0; - - expect(expectedCount).toBe(headerEls.length); - }) - .then(done) - .catch(done.fail); - }); - - it('does not show when recentBoards api returns empty array', done => { - vm.$nextTick() - .then(() => { - vm.recentBoards = []; - }) - .then(vm.$nextTick) - .then(() => { - const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); - const expectedCount = 0; - - expect(expectedCount).toBe(headerEls.length); - }) - .then(done) - .catch(done.fail); - }); - - it('does not show when search is active', done => { - fillSearchBox('Random string'); - - vm.$nextTick() - .then(() => { - const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); - const expectedCount = 0; - - expect(expectedCount).toBe(headerEls.length); - }) - .then(done) - .catch(done.fail); - }); - }); -}); diff --git a/spec/lib/sentry/client/issue_spec.rb b/spec/lib/sentry/client/issue_spec.rb index 2762c5b5cb9..d35e4b83d7f 100644 --- a/spec/lib/sentry/client/issue_spec.rb +++ b/spec/lib/sentry/client/issue_spec.rb @@ -49,7 +49,7 @@ describe Sentry::Client::Issue do it_behaves_like 'calls sentry api' it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error - it_behaves_like 'issues have correct length', 1 + it_behaves_like 'issues have correct length', 2 shared_examples 'has correct external_url' do context 'external_url' do @@ -184,7 +184,7 @@ describe Sentry::Client::Issue do it_behaves_like 'calls sentry api' it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error - it_behaves_like 'issues have correct length', 1 + it_behaves_like 'issues have correct length', 2 end context 'when cursor is present' do @@ -194,7 +194,7 @@ describe Sentry::Client::Issue do it_behaves_like 'calls sentry api' it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error - it_behaves_like 'issues have correct length', 1 + it_behaves_like 'issues have correct length', 2 end end diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index c086ab23058..754e26dcec8 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -102,7 +102,7 @@ describe Clusters::Applications::Ingress do it 'is initialized with ingress arguments' do expect(subject.name).to eq('ingress') expect(subject.chart).to eq('stable/nginx-ingress') - expect(subject.version).to eq('1.29.3') + expect(subject.version).to eq('1.29.7') expect(subject).to be_rbac expect(subject.files).to eq(ingress.files) end @@ -119,7 +119,7 @@ describe Clusters::Applications::Ingress do let(:ingress) { create(:clusters_applications_ingress, :errored, version: 'nginx') } it 'is initialized with the locked version' do - expect(subject.version).to eq('1.29.3') + expect(subject.version).to eq('1.29.7') end end end @@ -135,6 +135,7 @@ describe Clusters::Applications::Ingress do expect(values).to include('repository') expect(values).to include('stats') expect(values).to include('podAnnotations') + expect(values).to include('clusterIP') end end diff --git a/spec/models/user_detail_spec.rb b/spec/models/user_detail_spec.rb new file mode 100644 index 00000000000..3b6de5bb390 --- /dev/null +++ b/spec/models/user_detail_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe UserDetail do + it { is_expected.to belong_to(:user) } + + describe 'validations' do + describe 'job_title' do + it { is_expected.to validate_presence_of(:job_title) } + it { is_expected.to validate_length_of(:job_title).is_at_most(200) } + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 68cf41ce8a4..b8abe698639 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -29,6 +29,7 @@ describe User, :do_not_mock_admin_mode do it { is_expected.to have_one(:namespace) } it { is_expected.to have_one(:status) } it { is_expected.to have_one(:max_access_level_membership) } + it { is_expected.to have_one(:user_detail) } it { is_expected.to have_many(:snippets).dependent(:destroy) } it { is_expected.to have_many(:members) } it { is_expected.to have_many(:project_members) } @@ -4318,4 +4319,19 @@ describe User, :do_not_mock_admin_mode do expect(user.hook_attrs).to eq(user_attributes) end end + + describe 'user detail' do + context 'when user is initialized' do + let(:user) { build(:user) } + + it { expect(user.user_detail).to be_present } + it { expect(user.user_detail).not_to be_persisted } + end + + context 'when user detail exists' do + let(:user) { create(:user, job_title: 'Engineer') } + + it { expect(user.user_detail).to be_persisted } + end + end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 5a302f0528e..73bdd3b5d96 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -330,6 +330,21 @@ describe API::Users, :do_not_mock_admin_mode do expect(json_response.keys).not_to include 'last_sign_in_ip' end + context 'when job title is present' do + let(:job_title) { 'Fullstack Engineer' } + + before do + create(:user_detail, user: user, job_title: job_title) + end + + it 'returns job title of a user' do + get api("/users/#{user.id}", user) + + expect(response).to match_response_schema('public_api/v4/user/basic') + expect(json_response['job_title']).to eq(job_title) + end + end + context 'when authenticated as admin' do it 'includes the `is_admin` field' do get api("/users/#{user.id}", admin) diff --git a/spec/services/clusters/update_service_spec.rb b/spec/services/clusters/update_service_spec.rb index fdbed4fa5d8..d487edd8850 100644 --- a/spec/services/clusters/update_service_spec.rb +++ b/spec/services/clusters/update_service_spec.rb @@ -86,7 +86,7 @@ describe Clusters::UpdateService do it 'rejects changes' do is_expected.to eq(false) - expect(cluster.errors.full_messages).to include('cannot modify during creation') + expect(cluster.errors.full_messages).to include('Cannot modify provider during creation') end end end diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb index 5cd6283ca96..24738a79045 100644 --- a/spec/services/users/update_service_spec.rb +++ b/spec/services/users/update_service_spec.rb @@ -64,6 +64,13 @@ describe Users::UpdateService do end.not_to change { user.name } end + it 'updates user detail with provided attributes' do + result = update_user(user, job_title: 'Backend Engineer') + + expect(result).to eq(status: :success) + expect(user.job_title).to eq('Backend Engineer') + end + def update_user(user, opts) described_class.new(user, opts.merge(user: user)).execute end diff --git a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb index 30ba8d9b436..53183ac89f8 100644 --- a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb @@ -109,7 +109,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name| end expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/) + expect(json_response['message']['base'].first).to eq(_('Time to subtract exceeds the total time spent')) end end end |