summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-03-03 09:07:54 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-03-03 09:07:54 +0000
commit87ef501eacd66d7166183d20d84e33de022f7002 (patch)
treefa4e0f41e00a4b6aeb035530be4b5473f51b7a3d /spec
parentf321e51f46bcb628c3e96a44b5ebf3bb1c4033ab (diff)
downloadgitlab-ce-87ef501eacd66d7166183d20d84e33de022f7002.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/profiles_controller_spec.rb10
-rw-r--r--spec/factories/user_details.rb8
-rw-r--r--spec/features/error_tracking/user_searches_sentry_errors_spec.rb40
-rw-r--r--spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb4
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb22
-rw-r--r--spec/fixtures/sentry/error_list_search_response.json42
-rw-r--r--spec/fixtures/sentry/issues_sample_response.json48
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js155
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js29
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js6
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap17
-rw-r--r--spec/frontend/monitoring/components/charts/options_spec.js60
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js13
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js13
-rw-r--r--spec/frontend/monitoring/mock_data.js7
-rw-r--r--spec/frontend/monitoring/store/utils_spec.js98
-rw-r--r--spec/javascripts/boards/components/boards_selector_spec.js203
-rw-r--r--spec/lib/sentry/client/issue_spec.rb6
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb5
-rw-r--r--spec/models/user_detail_spec.rb14
-rw-r--r--spec/models/user_spec.rb16
-rw-r--r--spec/requests/api/users_spec.rb15
-rw-r--r--spec/services/clusters/update_service_spec.rb2
-rw-r--r--spec/services/users/update_service_spec.rb7
-rw-r--r--spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb2
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