summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects_controller_spec.rb22
-rw-r--r--spec/experiments/new_project_sast_enabled_experiment_spec.rb20
-rw-r--r--spec/features/admin/admin_dev_ops_reports_spec.rb (renamed from spec/features/admin/admin_dev_ops_report_spec.rb)10
-rw-r--r--spec/features/projects/user_creates_project_spec.rb23
-rw-r--r--spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js49
-rw-r--r--spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js57
-rw-r--r--spec/frontend/jobs/components/table/job_table_app_spec.js105
-rw-r--r--spec/frontend/jobs/mock_data.js2
-rw-r--r--spec/frontend/pages/import/history/components/import_error_details_spec.js66
-rw-r--r--spec/frontend/pages/import/history/components/import_history_app_spec.js205
-rw-r--r--spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js137
-rw-r--r--spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js53
-rw-r--r--spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js1
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_home_spec.js99
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js18
-rw-r--r--spec/frontend/runner/components/cells/runner_actions_cell_spec.js10
-rw-r--r--spec/frontend/runner/components/runner_pause_button_spec.js4
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js20
-rw-r--r--spec/frontend/security_configuration/components/app_spec.js14
-rw-r--r--spec/routing/admin_routing_spec.rb11
20 files changed, 684 insertions, 242 deletions
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index c098ea71f7a..d0aff6e282a 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -473,28 +473,6 @@ RSpec.describe ProjectsController do
end
end
end
-
- context 'with new_project_sast_enabled', :experiment do
- let(:params) do
- {
- path: 'foo',
- description: 'bar',
- namespace_id: user.namespace.id,
- initialize_with_sast: '1'
- }
- end
-
- it 'tracks an event on project creation' do
- expect(experiment(:new_project_sast_enabled)).to track(:created,
- property: 'blank',
- checked: true,
- project: an_instance_of(Project),
- namespace: user.namespace
- ).on_next_instance.with_context(user: user)
-
- post :create, params: { project: params }
- end
- end
end
describe 'GET edit' do
diff --git a/spec/experiments/new_project_sast_enabled_experiment_spec.rb b/spec/experiments/new_project_sast_enabled_experiment_spec.rb
deleted file mode 100644
index 041e5dfa469..00000000000
--- a/spec/experiments/new_project_sast_enabled_experiment_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe NewProjectSastEnabledExperiment do
- it "defines the expected behaviors and variants" do
- expect(subject.variant_names).to match_array([
- :candidate,
- :free_indicator,
- :unchecked_candidate,
- :unchecked_free_indicator
- ])
- end
-
- it "publishes to the database" do
- expect(subject).to receive(:publish_to_database)
-
- subject.publish
- end
-end
diff --git a/spec/features/admin/admin_dev_ops_report_spec.rb b/spec/features/admin/admin_dev_ops_reports_spec.rb
index cee79f8f440..bf32819cb52 100644
--- a/spec/features/admin/admin_dev_ops_report_spec.rb
+++ b/spec/features/admin/admin_dev_ops_reports_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'DevOps Report page', :js do
end
it 'has dismissable intro callout' do
- visit admin_dev_ops_report_path
+ visit admin_dev_ops_reports_path
expect(page).to have_content 'Introducing Your DevOps Report'
@@ -32,13 +32,13 @@ RSpec.describe 'DevOps Report page', :js do
end
it 'shows empty state' do
- visit admin_dev_ops_report_path
+ visit admin_dev_ops_reports_path
expect(page).to have_text('Service ping is off')
end
it 'hides the intro callout' do
- visit admin_dev_ops_report_path
+ visit admin_dev_ops_reports_path
expect(page).not_to have_content 'Introducing Your DevOps Report'
end
@@ -48,7 +48,7 @@ RSpec.describe 'DevOps Report page', :js do
it 'shows empty state' do
stub_application_setting(usage_ping_enabled: true)
- visit admin_dev_ops_report_path
+ visit admin_dev_ops_reports_path
expect(page).to have_content('Data is still calculating')
end
@@ -59,7 +59,7 @@ RSpec.describe 'DevOps Report page', :js do
stub_application_setting(usage_ping_enabled: true)
create(:dev_ops_report_metric)
- visit admin_dev_ops_report_path
+ visit admin_dev_ops_reports_path
expect(page).to have_selector('[data-testid="devops-score-app"]')
end
diff --git a/spec/features/projects/user_creates_project_spec.rb b/spec/features/projects/user_creates_project_spec.rb
index 6491a7425f7..84977b6c962 100644
--- a/spec/features/projects/user_creates_project_spec.rb
+++ b/spec/features/projects/user_creates_project_spec.rb
@@ -33,29 +33,6 @@ RSpec.describe 'User creates a project', :js do
end
it 'creates a new project that is not blank' do
- stub_experiments(new_project_sast_enabled: 'candidate')
-
- visit(new_project_path)
-
- click_link 'Create blank project'
- fill_in(:project_name, with: 'With initial commits')
-
- expect(page).to have_checked_field 'Initialize repository with a README'
- expect(page).to have_checked_field 'Enable Static Application Security Testing (SAST)'
-
- click_button('Create project')
-
- project = Project.last
-
- expect(page).to have_current_path(project_path(project), ignore_query: true)
- expect(page).to have_content('With initial commits')
- expect(page).to have_content('Configure SAST in `.gitlab-ci.yml`, creating this file if it does not already exist')
- expect(page).to have_content('README.md Initial commit')
- end
-
- it 'allows creating a new project when the new_project_sast_enabled is assigned the unchecked candidate' do
- stub_experiments(new_project_sast_enabled: 'unchecked_candidate')
-
visit(new_project_path)
click_link 'Create blank project'
diff --git a/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js b/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
new file mode 100644
index 00000000000..322cfa3ba1f
--- /dev/null
+++ b/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
@@ -0,0 +1,49 @@
+import { GlFilteredSearch } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue';
+import { mockFailedSearchToken } from '../../mock_data';
+
+describe('Jobs filtered search', () => {
+ let wrapper;
+
+ const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
+ const getSearchToken = (type) =>
+ findFilteredSearch()
+ .props('availableTokens')
+ .find((token) => token.type === type);
+
+ const findStatusToken = () => getSearchToken('status');
+
+ const createComponent = () => {
+ wrapper = shallowMount(JobsFilteredSearch);
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays filtered search', () => {
+ expect(findFilteredSearch().exists()).toBe(true);
+ });
+
+ it('displays status token', () => {
+ expect(findStatusToken()).toMatchObject({
+ type: 'status',
+ icon: 'status',
+ title: 'Status',
+ unique: true,
+ operators: OPERATOR_IS_ONLY,
+ });
+ });
+
+ it('emits filter token to parent component', () => {
+ findFilteredSearch().vm.$emit('submit', mockFailedSearchToken);
+
+ expect(wrapper.emitted('filterJobsBySearch')).toEqual([[mockFailedSearchToken]]);
+ });
+});
diff --git a/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
new file mode 100644
index 00000000000..ce8e482cc16
--- /dev/null
+++ b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
@@ -0,0 +1,57 @@
+import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { stubComponent } from 'helpers/stub_component';
+import JobStatusToken from '~/jobs/components/filtered_search/tokens/job_status_token.vue';
+
+describe('Job Status Token', () => {
+ let wrapper;
+
+ const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken);
+ const findAllFilteredSearchSuggestions = () =>
+ wrapper.findAllComponents(GlFilteredSearchSuggestion);
+ const findAllGlIcons = () => wrapper.findAllComponents(GlIcon);
+
+ const defaultProps = {
+ config: {
+ type: 'status',
+ icon: 'status',
+ title: 'Status',
+ unique: true,
+ },
+ value: {
+ data: '',
+ },
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMount(JobStatusToken, {
+ propsData: {
+ ...defaultProps,
+ },
+ stubs: {
+ GlFilteredSearchToken: stubComponent(GlFilteredSearchToken, {
+ template: `<div><slot name="suggestions"></slot></div>`,
+ }),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('passes config correctly', () => {
+ expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
+ });
+
+ it('renders all job statuses available', () => {
+ const expectedLength = 11;
+
+ expect(findAllFilteredSearchSuggestions()).toHaveLength(expectedLength);
+ expect(findAllGlIcons()).toHaveLength(expectedLength);
+ });
+});
diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/jobs/components/table/job_table_app_spec.js
index 4d51624dfff..98d8419b26e 100644
--- a/spec/frontend/jobs/components/table/job_table_app_spec.js
+++ b/spec/frontend/jobs/components/table/job_table_app_spec.js
@@ -1,30 +1,48 @@
-import { GlSkeletonLoader, GlAlert, GlEmptyState, GlIntersectionObserver } from '@gitlab/ui';
+import {
+ GlSkeletonLoader,
+ GlAlert,
+ GlEmptyState,
+ GlIntersectionObserver,
+ GlLoadingIcon,
+} from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { s__ } from '~/locale';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
import getJobsQuery from '~/jobs/components/table/graphql/queries/get_jobs.query.graphql';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue';
import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue';
-import { mockJobsQueryResponse, mockJobsQueryEmptyResponse } from '../../mock_data';
+import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue';
+import {
+ mockJobsQueryResponse,
+ mockJobsQueryEmptyResponse,
+ mockFailedSearchToken,
+} from '../../mock_data';
const projectPath = 'gitlab-org/gitlab';
Vue.use(VueApollo);
+jest.mock('~/flash');
+
describe('Job table app', () => {
let wrapper;
+ let jobsTableVueSearch = true;
const successHandler = jest.fn().mockResolvedValue(mockJobsQueryResponse);
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const emptyHandler = jest.fn().mockResolvedValue(mockJobsQueryEmptyResponse);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
const findTable = () => wrapper.findComponent(JobsTable);
const findTabs = () => wrapper.findComponent(JobsTableTabs);
const findAlert = () => wrapper.findComponent(GlAlert);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findFilteredSearch = () => wrapper.findComponent(JobsFilteredSearch);
const triggerInfiniteScroll = () =>
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
@@ -48,6 +66,7 @@ describe('Job table app', () => {
},
provide: {
fullPath: projectPath,
+ glFeatures: { jobsTableVueSearch },
},
apolloProvider: createMockApolloProvider(handler),
});
@@ -58,11 +77,21 @@ describe('Job table app', () => {
});
describe('loading state', () => {
- it('should display skeleton loader when loading', () => {
+ beforeEach(() => {
createComponent();
+ });
+ it('should display skeleton loader when loading', () => {
expect(findSkeletonLoader().exists()).toBe(true);
expect(findTable().exists()).toBe(false);
+ expect(findLoadingSpinner().exists()).toBe(false);
+ });
+
+ it('when switching tabs only the skeleton loader should show', () => {
+ findTabs().vm.$emit('fetchJobsByStatus', 'PENDING');
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+ expect(findLoadingSpinner().exists()).toBe(false);
});
});
@@ -76,6 +105,7 @@ describe('Job table app', () => {
it('should display the jobs table with data', () => {
expect(findTable().exists()).toBe(true);
expect(findSkeletonLoader().exists()).toBe(false);
+ expect(findLoadingSpinner().exists()).toBe(false);
});
it('should refetch jobs query on fetchJobsByStatus event', async () => {
@@ -98,8 +128,12 @@ describe('Job table app', () => {
});
it('handles infinite scrolling by calling fetch more', async () => {
+ expect(findLoadingSpinner().exists()).toBe(true);
+
await waitForPromises();
+ expect(findLoadingSpinner().exists()).toBe(false);
+
expect(successHandler).toHaveBeenCalledWith({
after: 'eyJpZCI6IjIzMTcifQ',
fullPath: 'gitlab-org/gitlab',
@@ -137,4 +171,69 @@ describe('Job table app', () => {
expect(findTable().exists()).toBe(true);
});
});
+
+ describe('filtered search', () => {
+ it('should display filtered search', () => {
+ createComponent();
+
+ expect(findFilteredSearch().exists()).toBe(true);
+ });
+
+ // this test should be updated once BE supports tab and filtered search filtering
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/356210
+ it.each`
+ scope | shouldDisplay
+ ${null} | ${true}
+ ${'PENDING'} | ${false}
+ ${'RUNNING'} | ${false}
+ ${['FAILED', 'SUCCESS', 'CANCELED']} | ${false}
+ `(
+ 'with tab scope $scope the filtered search displays $shouldDisplay',
+ async ({ scope, shouldDisplay }) => {
+ createComponent();
+
+ await findTabs().vm.$emit('fetchJobsByStatus', scope);
+
+ expect(findFilteredSearch().exists()).toBe(shouldDisplay);
+ },
+ );
+
+ it('refetches jobs query when filtering', async () => {
+ createComponent();
+
+ jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows raw text warning when user inputs raw text', async () => {
+ const expectedWarning = {
+ message: s__(
+ 'Jobs|Raw text search is not currently supported for the jobs filtered search feature. Please use the available search tokens.',
+ ),
+ type: 'warning',
+ };
+
+ createComponent();
+
+ jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', ['raw text']);
+
+ expect(createFlash).toHaveBeenCalledWith(expectedWarning);
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+ });
+
+ it('should not display filtered search', () => {
+ jobsTableVueSearch = false;
+
+ createComponent();
+
+ expect(findFilteredSearch().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js
index 73b9df1853d..b4cc58a04cc 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/jobs/mock_data.js
@@ -1918,3 +1918,5 @@ export const CIJobConnectionExistingCache = {
],
statuses: 'PENDING',
};
+
+export const mockFailedSearchToken = { type: 'status', value: { data: 'FAILED', operator: '=' } };
diff --git a/spec/frontend/pages/import/history/components/import_error_details_spec.js b/spec/frontend/pages/import/history/components/import_error_details_spec.js
new file mode 100644
index 00000000000..4ff3f0361cf
--- /dev/null
+++ b/spec/frontend/pages/import/history/components/import_error_details_spec.js
@@ -0,0 +1,66 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import ImportErrorDetails from '~/pages/import/history/components/import_error_details.vue';
+
+describe('ImportErrorDetails', () => {
+ const FAKE_ID = 5;
+ const API_URL = `/api/v4/projects/${FAKE_ID}`;
+
+ let wrapper;
+ let mock;
+
+ function createComponent({ shallow = true } = {}) {
+ const mountFn = shallow ? shallowMount : mount;
+ wrapper = mountFn(ImportErrorDetails, {
+ propsData: {
+ id: FAKE_ID,
+ },
+ });
+ }
+
+ const originalApiVersion = gon.api_version;
+ beforeAll(() => {
+ gon.api_version = 'v4';
+ });
+
+ afterAll(() => {
+ gon.api_version = originalApiVersion;
+ });
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ });
+
+ describe('general behavior', () => {
+ it('renders loading state when loading', () => {
+ createComponent();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('renders import_error if it is available', async () => {
+ const FAKE_IMPORT_ERROR = 'IMPORT ERROR';
+ mock.onGet(API_URL).reply(200, { import_error: FAKE_IMPORT_ERROR });
+ createComponent();
+ await axios.waitForAll();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find('pre').text()).toBe(FAKE_IMPORT_ERROR);
+ });
+
+ it('renders default text if error is not available', async () => {
+ mock.onGet(API_URL).reply(200, { import_error: null });
+ createComponent();
+ await axios.waitForAll();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find('pre').text()).toBe('No additional information provided.');
+ });
+ });
+});
diff --git a/spec/frontend/pages/import/history/components/import_history_app_spec.js b/spec/frontend/pages/import/history/components/import_history_app_spec.js
new file mode 100644
index 00000000000..0d821b114cf
--- /dev/null
+++ b/spec/frontend/pages/import/history/components/import_history_app_spec.js
@@ -0,0 +1,205 @@
+import { GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import ImportErrorDetails from '~/pages/import/history/components/import_error_details.vue';
+import ImportHistoryApp from '~/pages/import/history/components/import_history_app.vue';
+import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+
+describe('ImportHistoryApp', () => {
+ const API_URL = '/api/v4/projects.json';
+
+ const DEFAULT_HEADERS = {
+ 'x-page': 1,
+ 'x-per-page': 20,
+ 'x-next-page': 2,
+ 'x-total': 22,
+ 'x-total-pages': 2,
+ 'x-prev-page': null,
+ };
+ const DUMMY_RESPONSE = [
+ {
+ id: 1,
+ path_with_namespace: 'root/imported',
+ created_at: '2022-03-10T15:10:03.172Z',
+ import_url: null,
+ import_type: 'gitlab_project',
+ import_status: 'finished',
+ },
+ {
+ id: 2,
+ name_with_namespace: 'Administrator / Dummy',
+ path_with_namespace: 'root/dummy',
+ created_at: '2022-03-09T11:23:04.974Z',
+ import_url: 'https://dummy.github/url',
+ import_type: 'github',
+ import_status: 'failed',
+ },
+ {
+ id: 3,
+ name_with_namespace: 'Administrator / Dummy',
+ path_with_namespace: 'root/dummy2',
+ created_at: '2022-03-09T11:23:04.974Z',
+ import_url: 'git://non-http.url',
+ import_type: 'gi',
+ import_status: 'finished',
+ },
+ ];
+ let wrapper;
+ let mock;
+
+ function createComponent({ shallow = true } = {}) {
+ const mountFn = shallow ? shallowMount : mount;
+ wrapper = mountFn(ImportHistoryApp, {
+ provide: { assets: { gitlabLogo: 'http://dummy.host' } },
+ stubs: shallow ? { GlTable: { ...stubComponent(GlTable), props: ['items'] } } : {},
+ });
+ }
+
+ const originalApiVersion = gon.api_version;
+ beforeAll(() => {
+ gon.api_version = 'v4';
+ });
+
+ afterAll(() => {
+ gon.api_version = originalApiVersion;
+ });
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ });
+
+ describe('general behavior', () => {
+ it('renders loading state when loading', () => {
+ createComponent();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('renders empty state when no data is available', async () => {
+ mock.onGet(API_URL).reply(200, [], DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find(GlEmptyState).exists()).toBe(true);
+ });
+
+ it('renders table with data when history is available', async () => {
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+
+ const table = wrapper.find(GlTable);
+ expect(table.exists()).toBe(true);
+ expect(table.props().items).toStrictEqual(DUMMY_RESPONSE);
+ });
+
+ it('changes page when requested by pagination bar', async () => {
+ const NEW_PAGE = 4;
+
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+ mock.resetHistory();
+
+ const FAKE_NEXT_PAGE_REPLY = [
+ {
+ id: 4,
+ path_with_namespace: 'root/some_other_project',
+ created_at: '2022-03-10T15:10:03.172Z',
+ import_url: null,
+ import_type: 'gitlab_project',
+ import_status: 'finished',
+ },
+ ];
+
+ mock.onGet(API_URL).reply(200, FAKE_NEXT_PAGE_REPLY, DEFAULT_HEADERS);
+
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page', NEW_PAGE);
+ await axios.waitForAll();
+
+ expect(mock.history.get.length).toBe(1);
+ expect(mock.history.get[0].params).toStrictEqual(expect.objectContaining({ page: NEW_PAGE }));
+ expect(wrapper.find(GlTable).props().items).toStrictEqual(FAKE_NEXT_PAGE_REPLY);
+ });
+ });
+
+ it('changes page size when requested by pagination bar', async () => {
+ const NEW_PAGE_SIZE = 4;
+
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+ mock.resetHistory();
+
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page-size', NEW_PAGE_SIZE);
+ await axios.waitForAll();
+
+ expect(mock.history.get.length).toBe(1);
+ expect(mock.history.get[0].params).toStrictEqual(
+ expect.objectContaining({ per_page: NEW_PAGE_SIZE }),
+ );
+ });
+
+ it('resets page to 1 when page size is changed', async () => {
+ const NEW_PAGE_SIZE = 4;
+
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page', 2);
+ await axios.waitForAll();
+ mock.resetHistory();
+
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page-size', NEW_PAGE_SIZE);
+ await axios.waitForAll();
+
+ expect(mock.history.get.length).toBe(1);
+ expect(mock.history.get[0].params).toStrictEqual(
+ expect.objectContaining({ per_page: NEW_PAGE_SIZE, page: 1 }),
+ );
+ });
+
+ describe('details button', () => {
+ beforeEach(() => {
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent({ shallow: false });
+ return axios.waitForAll();
+ });
+
+ it('renders details button if relevant item has failed', async () => {
+ expect(
+ extendedWrapper(wrapper.find('tbody').findAll('tr').at(1)).findByText('Details').exists(),
+ ).toBe(true);
+ });
+
+ it('does not render details button if relevant item does not failed', () => {
+ expect(
+ extendedWrapper(wrapper.find('tbody').findAll('tr').at(0)).findByText('Details').exists(),
+ ).toBe(false);
+ });
+
+ it('expands details when details button is clicked', async () => {
+ const ORIGINAL_ROW_INDEX = 1;
+ await extendedWrapper(wrapper.find('tbody').findAll('tr').at(ORIGINAL_ROW_INDEX))
+ .findByText('Details')
+ .trigger('click');
+
+ const detailsRowContent = wrapper
+ .find('tbody')
+ .findAll('tr')
+ .at(ORIGINAL_ROW_INDEX + 1)
+ .findComponent(ImportErrorDetails);
+
+ expect(detailsRowContent.exists()).toBe(true);
+ expect(detailsRowContent.props().id).toBe(DUMMY_RESPONSE[1].id);
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
index ba06f113120..33b53bf6a56 100644
--- a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
+++ b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
@@ -1,146 +1,27 @@
-import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue';
-import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue';
-import PipelineConfigReferenceCard from '~/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue';
-import VisualizeAndLintCard from '~/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue';
+import { GlDrawer } from '@gitlab/ui';
import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
-import { DRAWER_EXPANDED_KEY } from '~/pipeline_editor/constants';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
describe('Pipeline editor drawer', () => {
- useLocalStorageSpy();
-
let wrapper;
+ const findDrawer = () => wrapper.findComponent(GlDrawer);
+
const createComponent = () => {
- wrapper = shallowMount(PipelineEditorDrawer, {
- stubs: { LocalStorageSync },
- });
+ wrapper = shallowMount(PipelineEditorDrawer);
};
- const findFirstPipelineCard = () => wrapper.findComponent(FirstPipelineCard);
- const findGettingStartedCard = () => wrapper.findComponent(GettingStartedCard);
- const findPipelineConfigReferenceCard = () => wrapper.findComponent(PipelineConfigReferenceCard);
- const findToggleBtn = () => wrapper.findComponent(GlButton);
- const findVisualizeAndLintCard = () => wrapper.findComponent(VisualizeAndLintCard);
-
- const findArrowIcon = () => wrapper.find('[data-testid="toggle-icon"]');
- const findCollapseText = () => wrapper.find('[data-testid="collapse-text"]');
- const findDrawerContent = () => wrapper.find('[data-testid="drawer-content"]');
-
- const clickToggleBtn = async () => findToggleBtn().vm.$emit('click');
-
- const originalObjects = [];
-
- beforeEach(() => {
- originalObjects.push(window.gon, window.gl);
- });
-
afterEach(() => {
wrapper.destroy();
- localStorage.clear();
- [window.gon, window.gl] = originalObjects;
- });
-
- describe('default expanded state', () => {
- it('sets the drawer to be closed by default', async () => {
- createComponent();
- expect(findDrawerContent().exists()).toBe(false);
- });
- });
-
- describe('when the drawer is collapsed', () => {
- beforeEach(async () => {
- createComponent();
- });
-
- it('shows the left facing arrow icon', () => {
- expect(findArrowIcon().props('name')).toBe('chevron-double-lg-left');
- });
-
- it('does not show the collapse text', () => {
- expect(findCollapseText().exists()).toBe(false);
- });
-
- it('does not show the drawer content', () => {
- expect(findDrawerContent().exists()).toBe(false);
- });
-
- it('can open the drawer by clicking on the toggle button', async () => {
- expect(findDrawerContent().exists()).toBe(false);
-
- await clickToggleBtn();
-
- expect(findDrawerContent().exists()).toBe(true);
- });
- });
-
- describe('when the drawer is expanded', () => {
- beforeEach(async () => {
- createComponent();
- await clickToggleBtn();
- });
-
- it('shows the right facing arrow icon', () => {
- expect(findArrowIcon().props('name')).toBe('chevron-double-lg-right');
- });
-
- it('shows the collapse text', () => {
- expect(findCollapseText().exists()).toBe(true);
- });
-
- it('shows the drawer content', () => {
- expect(findDrawerContent().exists()).toBe(true);
- });
-
- it('shows all the introduction cards', () => {
- expect(findFirstPipelineCard().exists()).toBe(true);
- expect(findGettingStartedCard().exists()).toBe(true);
- expect(findPipelineConfigReferenceCard().exists()).toBe(true);
- expect(findVisualizeAndLintCard().exists()).toBe(true);
- });
-
- it('can close the drawer by clicking on the toggle button', async () => {
- expect(findDrawerContent().exists()).toBe(true);
-
- await clickToggleBtn();
-
- expect(findDrawerContent().exists()).toBe(false);
- });
});
- describe('local storage', () => {
- it('saves the drawer expanded value to local storage', async () => {
- localStorage.setItem(DRAWER_EXPANDED_KEY, 'false');
-
- createComponent();
- await clickToggleBtn();
-
- expect(localStorage.setItem.mock.calls).toEqual([
- [DRAWER_EXPANDED_KEY, 'false'],
- [DRAWER_EXPANDED_KEY, 'true'],
- ]);
- });
-
- it('loads the drawer collapsed when local storage is set to `false`, ', async () => {
- localStorage.setItem(DRAWER_EXPANDED_KEY, false);
- createComponent();
-
- await nextTick();
-
- expect(findDrawerContent().exists()).toBe(false);
- });
+ it('emits close event when closing the drawer', () => {
+ createComponent();
- it('loads the drawer expanded when local storage is set to `true`, ', async () => {
- localStorage.setItem(DRAWER_EXPANDED_KEY, true);
- createComponent();
+ expect(wrapper.emitted('close-drawer')).toBeUndefined();
- await nextTick();
+ findDrawer().vm.$emit('close');
- expect(findDrawerContent().exists()).toBe(true);
- });
+ expect(wrapper.emitted('close-drawer')).toHaveLength(1);
});
});
diff --git a/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js b/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js
index 3ee53d4a055..8f50325295e 100644
--- a/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js
+++ b/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js
@@ -1,5 +1,5 @@
-import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import CiEditorHeader from '~/pipeline_editor/components/editor/ci_editor_header.vue';
import {
@@ -11,11 +11,18 @@ describe('CI Editor Header', () => {
let wrapper;
let trackingSpy = null;
- const createComponent = () => {
- wrapper = shallowMount(CiEditorHeader, {});
+ const createComponent = ({ showDrawer = false } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(CiEditorHeader, {
+ propsData: {
+ showDrawer,
+ },
+ }),
+ );
};
- const findLinkBtn = () => wrapper.findComponent(GlButton);
+ const findLinkBtn = () => wrapper.findByTestId('template-repo-link');
+ const findHelpBtn = () => wrapper.findByTestId('drawer-toggle');
afterEach(() => {
wrapper.destroy();
@@ -50,4 +57,42 @@ describe('CI Editor Header', () => {
});
});
});
+
+ describe('help button', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('finds the help button', () => {
+ expect(findHelpBtn().exists()).toBe(true);
+ });
+
+ it('has the information-o icon', () => {
+ expect(findHelpBtn().props('icon')).toBe('information-o');
+ });
+
+ describe('when pipeline editor drawer is closed', () => {
+ it('emits open drawer event when clicked', () => {
+ createComponent({ showDrawer: false });
+
+ expect(wrapper.emitted('open-drawer')).toBeUndefined();
+
+ findHelpBtn().vm.$emit('click');
+
+ expect(wrapper.emitted('open-drawer')).toHaveLength(1);
+ });
+ });
+
+ describe('when pipeline editor drawer is open', () => {
+ it('emits close drawer event when clicked', () => {
+ createComponent({ showDrawer: true });
+
+ expect(wrapper.emitted('close-drawer')).toBeUndefined();
+
+ findHelpBtn().vm.$emit('click');
+
+ expect(wrapper.emitted('close-drawer')).toHaveLength(1);
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
index fee52db9b64..6dffb7e5470 100644
--- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -40,6 +40,7 @@ describe('Pipeline editor tabs component', () => {
ciConfigData: mockLintResponse,
ciFileContent: mockCiYml,
isNewCiConfigFile: true,
+ showDrawer: false,
...props,
},
data() {
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
index 6f969546171..98e2c17967c 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
@@ -1,6 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import { GlModal } from '@gitlab/ui';
+import { GlButton, GlDrawer, GlModal } from '@gitlab/ui';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import CiEditorHeader from '~/pipeline_editor/components/editor/ci_editor_header.vue';
import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
@@ -18,24 +20,26 @@ describe('Pipeline editor home wrapper', () => {
let wrapper;
const createComponent = ({ props = {}, glFeatures = {}, data = {}, stubs = {} } = {}) => {
- wrapper = shallowMount(PipelineEditorHome, {
- data: () => data,
- propsData: {
- ciConfigData: mockLintResponse,
- ciFileContent: mockCiYml,
- isCiConfigDataLoading: false,
- isNewCiConfigFile: false,
- ...props,
- },
- provide: {
- projectFullPath: '',
- totalBranches: 19,
- glFeatures: {
- ...glFeatures,
+ wrapper = extendedWrapper(
+ shallowMount(PipelineEditorHome, {
+ data: () => data,
+ propsData: {
+ ciConfigData: mockLintResponse,
+ ciFileContent: mockCiYml,
+ isCiConfigDataLoading: false,
+ isNewCiConfigFile: false,
+ ...props,
},
- },
- stubs,
- });
+ provide: {
+ projectFullPath: '',
+ totalBranches: 19,
+ glFeatures: {
+ ...glFeatures,
+ },
+ },
+ stubs,
+ }),
+ );
};
const findBranchSwitcher = () => wrapper.findComponent(BranchSwitcher);
@@ -45,6 +49,7 @@ describe('Pipeline editor home wrapper', () => {
const findPipelineEditorDrawer = () => wrapper.findComponent(PipelineEditorDrawer);
const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader);
const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs);
+ const findHelpBtn = () => wrapper.findByTestId('drawer-toggle');
afterEach(() => {
wrapper.destroy();
@@ -70,10 +75,6 @@ describe('Pipeline editor home wrapper', () => {
it('shows the commit section by default', () => {
expect(findCommitSection().exists()).toBe(true);
});
-
- it('show the pipeline drawer', () => {
- expect(findPipelineEditorDrawer().exists()).toBe(true);
- });
});
describe('modal when switching branch', () => {
@@ -175,4 +176,58 @@ describe('Pipeline editor home wrapper', () => {
});
});
});
+
+ describe('help drawer', () => {
+ const clickHelpBtn = async () => {
+ findHelpBtn().vm.$emit('click');
+ await nextTick();
+ };
+
+ it('hides the drawer by default', () => {
+ createComponent();
+
+ expect(findPipelineEditorDrawer().props('isVisible')).toBe(false);
+ });
+
+ it('toggles the drawer on button click', async () => {
+ createComponent({
+ stubs: {
+ CiEditorHeader,
+ GlButton,
+ GlDrawer,
+ PipelineEditorTabs,
+ PipelineEditorDrawer,
+ },
+ });
+
+ await clickHelpBtn();
+
+ expect(findPipelineEditorDrawer().props('isVisible')).toBe(true);
+
+ await clickHelpBtn();
+
+ expect(findPipelineEditorDrawer().props('isVisible')).toBe(false);
+ });
+
+ it("closes the drawer through the drawer's close button", async () => {
+ createComponent({
+ stubs: {
+ CiEditorHeader,
+ GlButton,
+ GlDrawer,
+ PipelineEditorTabs,
+ PipelineEditorDrawer,
+ },
+ });
+
+ await clickHelpBtn();
+
+ expect(findPipelineEditorDrawer().props('isVisible')).toBe(true);
+
+ findPipelineEditorDrawer().find(GlDrawer).vm.$emit('close');
+ await nextTick();
+
+ expect(findPipelineEditorDrawer().props('isVisible')).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
index cdaec0a3a8b..313870e8ea1 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -235,9 +235,11 @@ describe('AdminRunnersApp', () => {
const mockRunner = runnersData.data.runners.nodes[0];
const { id: graphqlId, shortSha } = mockRunner;
const id = getIdFromGraphQLId(graphqlId);
+ const COUNT_QUERIES = 7; // Smart queries that display a filtered count of runners
+ const FILTERED_COUNT_QUERIES = 4; // Smart queries that display a count of runners in tabs
beforeEach(async () => {
- mockRunnersQuery.mockClear();
+ mockRunnersCountQuery.mockClear();
createComponent({ mountFn: mountExtended });
showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show');
@@ -252,12 +254,18 @@ describe('AdminRunnersApp', () => {
expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${id}`);
});
- it('When runner is deleted, data is refetched and a toast message is shown', async () => {
- expect(mockRunnersQuery).toHaveBeenCalledTimes(1);
+ it('When runner is paused or unpaused, some data is refetched', async () => {
+ expect(mockRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES);
- findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
+ findRunnerActionsCell().vm.$emit('toggledPaused');
- expect(mockRunnersQuery).toHaveBeenCalledTimes(2);
+ expect(mockRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES + FILTERED_COUNT_QUERIES);
+
+ expect(showToast).toHaveBeenCalledTimes(0);
+ });
+
+ it('When runner is deleted, data is refetched and a toast message is shown', async () => {
+ findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Runner deleted');
diff --git a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
index 9ca99d1109b..7a949cb6505 100644
--- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
@@ -100,6 +100,16 @@ describe('RunnerActionsCell', () => {
expect(findDeleteBtn().props('runner')).toEqual(mockRunner);
});
+ it('Emits toggledPaused events', () => {
+ createComponent();
+
+ expect(wrapper.emitted('toggledPaused')).toBe(undefined);
+
+ findRunnerPauseBtn().vm.$emit('toggledPaused');
+
+ expect(wrapper.emitted('toggledPaused')).toHaveLength(1);
+ });
+
it('Emits delete events', () => {
const value = { name: 'Runner' };
diff --git a/spec/frontend/runner/components/runner_pause_button_spec.js b/spec/frontend/runner/components/runner_pause_button_spec.js
index 3d9df03977e..9ebb30b6ed7 100644
--- a/spec/frontend/runner/components/runner_pause_button_spec.js
+++ b/spec/frontend/runner/components/runner_pause_button_spec.js
@@ -146,6 +146,10 @@ describe('RunnerPauseButton', () => {
it('The button does not have a loading state', () => {
expect(findBtn().props('loading')).toBe(false);
});
+
+ it('The button emits toggledPaused', () => {
+ expect(wrapper.emitted('toggledPaused')).toHaveLength(1);
+ });
});
describe('When update fails', () => {
diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js
index 70e303e8626..6d7ecc4506a 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -193,9 +193,11 @@ describe('GroupRunnersApp', () => {
const { webUrl, editUrl, node } = mockGroupRunnersEdges[0];
const { id: graphqlId, shortSha } = node;
const id = getIdFromGraphQLId(graphqlId);
+ const COUNT_QUERIES = 6; // Smart queries that display a filtered count of runners
+ const FILTERED_COUNT_QUERIES = 3; // Smart queries that display a count of runners in tabs
beforeEach(async () => {
- mockGroupRunnersQuery.mockClear();
+ mockGroupRunnersCountQuery.mockClear();
createComponent({ mountFn: mountExtended });
showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show');
@@ -219,12 +221,20 @@ describe('GroupRunnersApp', () => {
});
});
- it('When runner is deleted, data is refetched and a toast is shown', async () => {
- expect(mockGroupRunnersQuery).toHaveBeenCalledTimes(1);
+ it('When runner is paused or unpaused, some data is refetched', async () => {
+ expect(mockGroupRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES);
- findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
+ findRunnerActionsCell().vm.$emit('toggledPaused');
+
+ expect(mockGroupRunnersCountQuery).toHaveBeenCalledTimes(
+ COUNT_QUERIES + FILTERED_COUNT_QUERIES,
+ );
- expect(mockGroupRunnersQuery).toHaveBeenCalledTimes(2);
+ expect(showToast).toHaveBeenCalledTimes(0);
+ });
+
+ it('When runner is deleted, data is refetched and a toast message is shown', async () => {
+ findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Runner deleted');
diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js
index 963577fa763..5787747b00b 100644
--- a/spec/frontend/security_configuration/components/app_spec.js
+++ b/spec/frontend/security_configuration/components/app_spec.js
@@ -1,4 +1,4 @@
-import { GlTab, GlTabs } from '@gitlab/ui';
+import { GlTab, GlTabs, GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
@@ -107,6 +107,7 @@ describe('App component', () => {
const findUpgradeBanner = () => wrapper.findComponent(UpgradeBanner);
const findAutoDevopsAlert = () => wrapper.findComponent(AutoDevopsAlert);
const findAutoDevopsEnabledAlert = () => wrapper.findComponent(AutoDevopsEnabledAlert);
+ const findVulnerabilityManagementTab = () => wrapper.findByTestId('vulnerability-management-tab');
const securityFeaturesMock = [
{
@@ -454,9 +455,16 @@ describe('App component', () => {
});
it('renders security training description', () => {
- const vulnerabilityManagementTab = wrapper.findByTestId('vulnerability-management-tab');
+ expect(findVulnerabilityManagementTab().text()).toContain(i18n.securityTrainingDescription);
+ });
+
+ it('renders link to help docs', () => {
+ const trainingLink = findVulnerabilityManagementTab().findComponent(GlLink);
- expect(vulnerabilityManagementTab.text()).toContain(i18n.securityTrainingDescription);
+ expect(trainingLink.text()).toBe('Learn more about vulnerability training');
+ expect(trainingLink.attributes('href')).toBe(
+ '/help/user/application_security/vulnerabilities/index#enable-security-training-for-vulnerabilities',
+ );
});
});
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index 8c36d7d4668..f48b4de23a2 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -134,10 +134,17 @@ RSpec.describe Admin::HealthCheckController, "routing" do
end
end
-# admin_dev_ops_report GET /admin/dev_ops_report(.:format) admin/dev_ops_report#show
+# admin_dev_ops_reports GET /admin/dev_ops_reports(.:format) admin/dev_ops_report#show
RSpec.describe Admin::DevOpsReportController, "routing" do
it "to #show" do
- expect(get("/admin/dev_ops_report")).to route_to('admin/dev_ops_report#show')
+ expect(get("/admin/dev_ops_reports")).to route_to('admin/dev_ops_report#show')
+ end
+
+ describe 'admin devops reports' do
+ include RSpec::Rails::RequestExampleGroup
+ it 'redirects from /admin/dev_ops_report to /admin/dev_ops_reports' do
+ expect(get("/admin/dev_ops_report")).to redirect_to(admin_dev_ops_reports_path)
+ end
end
end