Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/ci')
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js5
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js5
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js37
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js3
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js3
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js13
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js109
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js45
-rw-r--r--spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js1
-rw-r--r--spec/frontend/ci/pipeline_editor/mock_data.js62
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js3
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js107
-rw-r--r--spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js98
-rw-r--r--spec/frontend/ci/pipeline_new/mock_data.js1
-rw-r--r--spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js28
-rw-r--r--spec/frontend/ci/reports/codequality_report/store/actions_spec.js19
-rw-r--r--spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js80
-rw-r--r--spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js108
-rw-r--r--spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js18
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js15
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js10
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js9
-rw-r--r--spec/frontend/ci/runner/components/runner_bulk_delete_spec.js32
-rw-r--r--spec/frontend/ci/runner/components/runner_details_tabs_spec.js127
-rw-r--r--spec/frontend/ci/runner/components/runner_form_fields_spec.js87
-rw-r--r--spec/frontend/ci/runner/components/runner_header_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/runner_job_status_badge_spec.js19
-rw-r--r--spec/frontend/ci/runner/components/runner_jobs_table_spec.js8
-rw-r--r--spec/frontend/ci/runner/components/runner_list_empty_state_spec.js71
-rw-r--r--spec/frontend/ci/runner/components/runner_list_spec.js20
-rw-r--r--spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js96
-rw-r--r--spec/frontend/ci/runner/components/runner_platforms_radio_spec.js154
-rw-r--r--spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js10
-rw-r--r--spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js40
-rw-r--r--spec/frontend/ci/runner/group_runners/group_runners_app_spec.js14
-rw-r--r--spec/frontend/ci/runner/mock_data.js1
36 files changed, 1140 insertions, 324 deletions
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
index 3f1eebbc6a5..c0fb133b9b1 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
@@ -1,11 +1,10 @@
import { shallowMount } from '@vue/test-utils';
+import { TYPENAME_GROUP } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciGroupVariables from '~/ci/ci_variable_list/components/ci_group_variables.vue';
import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
-import { GRAPHQL_GROUP_TYPE } from '~/ci/ci_variable_list/constants';
-
const mockProvide = {
glFeatures: {
groupScopedCiVariables: false,
@@ -36,7 +35,7 @@ describe('Ci Group Variable wrapper', () => {
it('are passed down the correctly to ci_variable_shared', () => {
expect(findCiShared().props()).toEqual({
- id: convertToGraphQLId(GRAPHQL_GROUP_TYPE, mockProvide.groupId),
+ id: convertToGraphQLId(TYPENAME_GROUP, mockProvide.groupId),
areScopedVariablesAvailable: false,
componentName: 'GroupVariables',
entity: 'group',
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
index 7230017c560..bd1e6b17d6b 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
@@ -1,11 +1,10 @@
import { shallowMount } from '@vue/test-utils';
+import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciProjectVariables from '~/ci/ci_variable_list/components/ci_project_variables.vue';
import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
-import { GRAPHQL_PROJECT_TYPE } from '~/ci/ci_variable_list/constants';
-
const mockProvide = {
projectFullPath: '/namespace/project',
projectId: 1,
@@ -32,7 +31,7 @@ describe('Ci Project Variable wrapper', () => {
it('Passes down the correct props to ci_variable_shared', () => {
expect(findCiShared().props()).toEqual({
- id: convertToGraphQLId(GRAPHQL_PROJECT_TYPE, mockProvide.projectId),
+ id: convertToGraphQLId(TYPENAME_PROJECT, mockProvide.projectId),
areScopedVariablesAvailable: true,
componentName: 'ProjectVariables',
entity: 'project',
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
index 7838e4884d8..508af964ca3 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
@@ -21,6 +21,8 @@ describe('Ci variable modal', () => {
let trackingSpy;
const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$';
+ const maskableRawRegex = '^\\S{8,}$';
+
const mockVariables = mockVariablesWithScopes(instanceString);
const defaultProvide = {
@@ -30,10 +32,13 @@ describe('Ci variable modal', () => {
awsTipLearnLink: '/learn-link',
containsVariableReferenceLink: '/reference',
environmentScopeLink: '/help/environments',
+ glFeatures: {
+ ciRemoveCharacterLimitationRawMaskedVar: true,
+ },
isProtectedByDefault: false,
maskedEnvironmentVariablesLink: '/variables-link',
+ maskableRawRegex,
maskableRegex,
- protectedEnvironmentVariablesLink: '/protected-link',
};
const defaultProps = {
@@ -424,6 +429,36 @@ describe('Ci variable modal', () => {
describe('Validations', () => {
const maskError = 'This variable can not be masked.';
+ describe('when the variable is raw', () => {
+ const [variable] = mockVariables;
+ const validRawMaskedVariable = {
+ ...variable,
+ value: 'd$%^asdsadas',
+ masked: false,
+ raw: true,
+ };
+
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ props: { selectedVariable: validRawMaskedVariable },
+ });
+ });
+
+ it('should not show an error with symbols', async () => {
+ await findMaskedVariableCheckbox().trigger('click');
+
+ expect(findModal().text()).not.toContain(maskError);
+ });
+
+ it('should not show an error when length is less than 8', async () => {
+ await findValueField().vm.$emit('input', 'a');
+ await findMaskedVariableCheckbox().trigger('click');
+
+ expect(findModal().text()).toContain(maskError);
+ });
+ });
+
describe('when the mask state is invalid', () => {
beforeEach(async () => {
const [variable] = mockVariables;
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
index 2d39bff8ce0..c977ae773db 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
@@ -6,6 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { resolvers } from '~/ci/ci_variable_list/graphql/settings';
+import { TYPENAME_GROUP } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
@@ -227,7 +228,7 @@ describe('Ci Variable Shared Component', () => {
variables: {
endpoint: mockProvide.endpoint,
fullPath: groupProps.fullPath,
- id: convertToGraphQLId('Group', groupProps.id),
+ id: convertToGraphQLId(TYPENAME_GROUP, groupProps.id),
variable: newVariable,
},
});
diff --git a/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
index d7f0ce838d6..dc72694d26f 100644
--- a/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
@@ -11,11 +11,12 @@ describe('CI Editor Header', () => {
let wrapper;
let trackingSpy = null;
- const createComponent = ({ showDrawer = false } = {}) => {
+ const createComponent = ({ showDrawer = false, showJobAssistantDrawer = false } = {}) => {
wrapper = extendedWrapper(
shallowMount(CiEditorHeader, {
propsData: {
showDrawer,
+ showJobAssistantDrawer,
},
}),
);
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
index 6f28362e478..7bf955012c7 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
@@ -47,11 +47,6 @@ describe('Pipeline Status', () => {
mockLinkedPipelinesQuery = jest.fn();
});
- afterEach(() => {
- mockLinkedPipelinesQuery.mockReset();
- wrapper.destroy();
- });
-
describe('when there are stages', () => {
beforeEach(() => {
createComponent();
@@ -74,9 +69,11 @@ describe('Pipeline Status', () => {
describe('when querying upstream and downstream pipelines', () => {
describe('when query succeeds', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mockLinkedPipelinesQuery.mockResolvedValue(mockLinkedPipelines());
createComponentWithApollo();
+
+ await waitForPromises();
});
it('should call the query with the correct variables', () => {
@@ -86,6 +83,10 @@ describe('Pipeline Status', () => {
iid: mockProjectPipeline().pipeline.iid,
});
});
+
+ it('renders only the latest downstream pipelines', () => {
+ expect(findPipelineMiniGraph().props('downstreamPipelines')).toHaveLength(1);
+ });
});
describe('when query fails', () => {
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
deleted file mode 100644
index 6f28362e478..00000000000
--- a/spec/frontend/ci/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
+++ /dev/null
@@ -1,109 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import PipelineEditorMiniGraph from '~/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
-import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
-import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
-import { PIPELINE_FAILURE } from '~/ci/pipeline_editor/constants';
-import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data';
-
-Vue.use(VueApollo);
-
-describe('Pipeline Status', () => {
- let wrapper;
- let mockApollo;
- let mockLinkedPipelinesQuery;
-
- const createComponent = ({ hasStages = true, options } = {}) => {
- wrapper = shallowMount(PipelineEditorMiniGraph, {
- provide: {
- dataMethod: 'graphql',
- projectFullPath: mockProjectFullPath,
- },
- propsData: {
- pipeline: mockProjectPipeline({ hasStages }).pipeline,
- },
- ...options,
- });
- };
-
- const createComponentWithApollo = (hasStages = true) => {
- const handlers = [[getLinkedPipelinesQuery, mockLinkedPipelinesQuery]];
- mockApollo = createMockApollo(handlers);
-
- createComponent({
- hasStages,
- options: {
- apolloProvider: mockApollo,
- },
- });
- };
-
- const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
-
- beforeEach(() => {
- mockLinkedPipelinesQuery = jest.fn();
- });
-
- afterEach(() => {
- mockLinkedPipelinesQuery.mockReset();
- wrapper.destroy();
- });
-
- describe('when there are stages', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders pipeline mini graph', () => {
- expect(findPipelineMiniGraph().exists()).toBe(true);
- });
- });
-
- describe('when there are no stages', () => {
- beforeEach(() => {
- createComponent({ hasStages: false });
- });
-
- it('does not render pipeline mini graph', () => {
- expect(findPipelineMiniGraph().exists()).toBe(false);
- });
- });
-
- describe('when querying upstream and downstream pipelines', () => {
- describe('when query succeeds', () => {
- beforeEach(() => {
- mockLinkedPipelinesQuery.mockResolvedValue(mockLinkedPipelines());
- createComponentWithApollo();
- });
-
- it('should call the query with the correct variables', () => {
- expect(mockLinkedPipelinesQuery).toHaveBeenCalledTimes(1);
- expect(mockLinkedPipelinesQuery).toHaveBeenCalledWith({
- fullPath: mockProjectFullPath,
- iid: mockProjectPipeline().pipeline.iid,
- });
- });
- });
-
- describe('when query fails', () => {
- beforeEach(async () => {
- mockLinkedPipelinesQuery.mockRejectedValue(new Error());
- createComponentWithApollo();
- await waitForPromises();
- });
-
- it('should emit an error event when query fails', async () => {
- expect(wrapper.emitted('showError')).toHaveLength(1);
- expect(wrapper.emitted('showError')[0]).toEqual([
- {
- type: PIPELINE_FAILURE,
- reasons: [wrapper.vm.$options.i18n.linkedPipelinesFetchError],
- },
- ]);
- });
- });
- });
-});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js
new file mode 100644
index 00000000000..79200d92598
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js
@@ -0,0 +1,45 @@
+import { GlDrawer } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import JobAssistantDrawer from '~/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+
+Vue.use(VueApollo);
+
+describe('Job assistant drawer', () => {
+ let wrapper;
+
+ const findDrawer = () => wrapper.findComponent(GlDrawer);
+
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
+
+ const createComponent = () => {
+ wrapper = mountExtended(JobAssistantDrawer, {
+ propsData: {
+ isVisible: true,
+ },
+ });
+ };
+
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('should emit close job assistant drawer event when closing the drawer', () => {
+ expect(wrapper.emitted('close-job-assistant-drawer')).toBeUndefined();
+
+ findDrawer().vm.$emit('close');
+
+ expect(wrapper.emitted('close-job-assistant-drawer')).toHaveLength(1);
+ });
+
+ it('should emit close job assistant drawer event when click cancel button', () => {
+ expect(wrapper.emitted('close-job-assistant-drawer')).toBeUndefined();
+
+ findCancelButton().trigger('click');
+
+ expect(wrapper.emitted('close-job-assistant-drawer')).toHaveLength(1);
+ });
+});
diff --git a/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
index 70310cbdb10..f40db50aab7 100644
--- a/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -56,6 +56,7 @@ describe('Pipeline editor tabs component', () => {
currentTab: CREATE_TAB,
isNewCiConfigFile: true,
showDrawer: false,
+ showJobAssistantDrawer: false,
...props,
},
data() {
diff --git a/spec/frontend/ci/pipeline_editor/mock_data.js b/spec/frontend/ci/pipeline_editor/mock_data.js
index 176dc24f169..541123d7efc 100644
--- a/spec/frontend/ci/pipeline_editor/mock_data.js
+++ b/spec/frontend/ci/pipeline_editor/mock_data.js
@@ -373,13 +373,64 @@ export const mockLinkedPipelines = ({ hasDownstream = true, hasUpstream = true }
{
id: 'gid://gitlab/Ci::Pipeline/612',
path: '/root/job-log-sections/-/pipelines/612',
- project: { name: 'job-log-sections', __typename: 'Project' },
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-612-612',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/532',
+ retried: false,
+ },
+ __typename: 'Pipeline',
+ },
+ {
+ id: 'gid://gitlab/Ci::Pipeline/611',
+ path: '/root/job-log-sections/-/pipelines/611',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
detailedStatus: {
+ id: 'success-611-611',
group: 'success',
icon: 'status_success',
label: 'passed',
__typename: 'DetailedStatus',
},
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/531',
+ retried: true,
+ },
+ __typename: 'Pipeline',
+ },
+ {
+ id: 'gid://gitlab/Ci::Pipeline/609',
+ path: '/root/job-log-sections/-/pipelines/609',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-609-609',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/530',
+ retried: true,
+ },
__typename: 'Pipeline',
},
],
@@ -391,8 +442,13 @@ export const mockLinkedPipelines = ({ hasDownstream = true, hasUpstream = true }
upstream = {
id: 'gid://gitlab/Ci::Pipeline/610',
path: '/root/trigger-downstream/-/pipelines/610',
- project: { name: 'trigger-downstream', __typename: 'Project' },
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'trigger-downstream',
+ __typename: 'Project',
+ },
detailedStatus: {
+ id: 'success-610-610',
group: 'success',
icon: 'status_success',
label: 'passed',
@@ -405,7 +461,9 @@ export const mockLinkedPipelines = ({ hasDownstream = true, hasUpstream = true }
return {
data: {
project: {
+ id: 'gid://gitlab/Project/21',
pipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/790',
path: '/root/ci-project/-/pipelines/790',
downstream,
upstream,
diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
index 2246d0bbf7e..a103acb33bc 100644
--- a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
@@ -5,6 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
import PipelineEditorTabs from '~/ci/pipeline_editor/components/pipeline_editor_tabs.vue';
@@ -343,7 +344,7 @@ describe('Pipeline editor app component', () => {
describe('when the lint query returns a 500 error', () => {
beforeEach(async () => {
- mockCiConfigData.mockRejectedValueOnce(new Error(500));
+ mockCiConfigData.mockRejectedValueOnce(new Error(HTTP_STATUS_INTERNAL_SERVER_ERROR));
await createComponentWithApollo({
stubs: { PipelineEditorHome, PipelineEditorHeader, ValidationSegment },
});
diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
index 621e015e825..4f8f2112abe 100644
--- a/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
@@ -6,6 +6,7 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import CiEditorHeader from '~/ci/pipeline_editor/components/editor/ci_editor_header.vue';
import CommitSection from '~/ci/pipeline_editor/components/commit/commit_section.vue';
import PipelineEditorDrawer from '~/ci/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
+import JobAssistantDrawer from '~/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue';
import PipelineEditorFileNav from '~/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
import PipelineEditorFileTree from '~/ci/pipeline_editor/components/file_tree/container.vue';
import BranchSwitcher from '~/ci/pipeline_editor/components/file_nav/branch_switcher.vue';
@@ -56,11 +57,13 @@ describe('Pipeline editor home wrapper', () => {
const findFileNav = () => wrapper.findComponent(PipelineEditorFileNav);
const findModal = () => wrapper.findComponent(GlModal);
const findPipelineEditorDrawer = () => wrapper.findComponent(PipelineEditorDrawer);
+ const findJobAssistantDrawer = () => wrapper.findComponent(JobAssistantDrawer);
const findPipelineEditorFileTree = () => wrapper.findComponent(PipelineEditorFileTree);
const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader);
const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs);
const findFileTreeBtn = () => wrapper.findByTestId('file-tree-toggle');
const findHelpBtn = () => wrapper.findByTestId('drawer-toggle');
+ const findJobAssistantBtn = () => wrapper.findByTestId('job-assistant-drawer-toggle');
afterEach(() => {
localStorage.clear();
@@ -261,6 +264,110 @@ describe('Pipeline editor home wrapper', () => {
});
});
+ describe('job assistant drawer', () => {
+ const clickHelpBtn = async () => {
+ findHelpBtn().vm.$emit('click');
+ await nextTick();
+ };
+ const clickJobAssistantBtn = async () => {
+ findJobAssistantBtn().vm.$emit('click');
+ await nextTick();
+ };
+
+ const stubs = {
+ CiEditorHeader,
+ GlButton,
+ GlDrawer,
+ PipelineEditorTabs,
+ JobAssistantDrawer,
+ };
+
+ it('hides the job assistant drawer by default', () => {
+ createComponent({
+ glFeatures: {
+ ciJobAssistantDrawer: true,
+ },
+ });
+
+ expect(findJobAssistantDrawer().props('isVisible')).toBe(false);
+ });
+
+ it('toggles the job assistant drawer on button click', async () => {
+ createComponent({
+ stubs,
+ glFeatures: {
+ ciJobAssistantDrawer: true,
+ },
+ });
+
+ await clickJobAssistantBtn();
+
+ expect(findJobAssistantDrawer().props('isVisible')).toBe(true);
+
+ await clickJobAssistantBtn();
+
+ expect(findJobAssistantDrawer().props('isVisible')).toBe(false);
+ });
+
+ it("closes the job assistant drawer through the drawer's close button", async () => {
+ createComponent({
+ stubs,
+ glFeatures: {
+ ciJobAssistantDrawer: true,
+ },
+ });
+
+ await clickJobAssistantBtn();
+
+ expect(findJobAssistantDrawer().props('isVisible')).toBe(true);
+
+ findJobAssistantDrawer().findComponent(GlDrawer).vm.$emit('close');
+ await nextTick();
+
+ expect(findJobAssistantDrawer().props('isVisible')).toBe(false);
+ });
+
+ it('covers helper drawer when opened last', async () => {
+ createComponent({
+ stubs: {
+ ...stubs,
+ PipelineEditorDrawer,
+ },
+ glFeatures: {
+ ciJobAssistantDrawer: true,
+ },
+ });
+
+ await clickHelpBtn();
+ await clickJobAssistantBtn();
+
+ const jobAssistantIndex = Number(findJobAssistantDrawer().props().zIndex);
+ const pipelineEditorDrawerIndex = Number(findPipelineEditorDrawer().props().zIndex);
+
+ expect(jobAssistantIndex).toBeGreaterThan(pipelineEditorDrawerIndex);
+ });
+
+ it('covered by helper drawer when opened first', async () => {
+ createComponent({
+ stubs: {
+ ...stubs,
+ PipelineEditorDrawer,
+ },
+ glFeatures: {
+ ciJobAssistantDrawer: true,
+ },
+ });
+
+ await clickJobAssistantBtn();
+ await clickHelpBtn();
+
+ const jobAssistantIndex = Number(findJobAssistantDrawer().props().zIndex);
+ const pipelineEditorDrawerIndex = Number(findPipelineEditorDrawer().props().zIndex);
+
+ expect(jobAssistantIndex).toBeLessThan(pipelineEditorDrawerIndex);
+ });
+ });
+
describe('file tree', () => {
const toggleFileTree = async () => {
findFileTreeBtn().vm.$emit('click');
diff --git a/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
index cd16045f92d..6f18899ebac 100644
--- a/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
@@ -14,7 +14,9 @@ import {
HTTP_STATUS_OK,
} from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
-import PipelineNewForm from '~/ci/pipeline_new/components/pipeline_new_form.vue';
+import PipelineNewForm, {
+ POLLING_INTERVAL,
+} from '~/ci/pipeline_new/components/pipeline_new_form.vue';
import ciConfigVariablesQuery from '~/ci/pipeline_new/graphql/queries/ci_config_variables.graphql';
import { resolvers } from '~/ci/pipeline_new/graphql/resolvers';
import RefsDropdown from '~/ci/pipeline_new/components/refs_dropdown.vue';
@@ -24,6 +26,7 @@ import {
mockCiConfigVariablesResponseWithoutDesc,
mockEmptyCiConfigVariablesResponse,
mockError,
+ mockNoCachedCiConfigVariablesResponse,
mockQueryParams,
mockPostParams,
mockProjectId,
@@ -69,6 +72,10 @@ describe('Pipeline New Form', () => {
const findCCAlert = () => wrapper.findComponent(CreditCardValidationRequiredAlert);
const getFormPostParams = () => JSON.parse(mock.history.post[0].data);
+ const advanceToNextFetch = (milliseconds) => {
+ jest.advanceTimersByTime(milliseconds);
+ };
+
const selectBranch = async (branch) => {
// Select a branch in the dropdown
findRefsDropdown().vm.$emit('input', {
@@ -266,17 +273,98 @@ describe('Pipeline New Form', () => {
});
});
- describe('when yml defines a variable', () => {
- it('loading icon is shown when content is requested and hidden when received', async () => {
- mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
- createComponentWithApollo({ props: mockQueryParams, method: mountExtended });
+ describe('When there are no variables in the API cache', () => {
+ beforeEach(async () => {
+ mockCiConfigVariables.mockResolvedValue(mockNoCachedCiConfigVariablesResponse);
+ createComponentWithApollo({ method: mountExtended });
+ await waitForPromises();
+ });
+ it('stops polling after CONFIG_VARIABLES_TIMEOUT ms have passed', async () => {
+ advanceToNextFetch(POLLING_INTERVAL);
+ await waitForPromises();
+
+ advanceToNextFetch(POLLING_INTERVAL);
+ await waitForPromises();
+
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(3);
+
+ advanceToNextFetch(POLLING_INTERVAL);
+ await waitForPromises();
+
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(3);
+ });
+
+ it('shows loading icon while query polls for updated values', async () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(1);
+
+ advanceToNextFetch(POLLING_INTERVAL);
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(2);
+ });
+
+ it('hides loading icon and stops polling after query fetches the updated values', async () => {
expect(findLoadingIcon().exists()).toBe(true);
+ mockCiConfigVariables.mockResolvedValue(mockCiConfigVariablesResponse);
+ advanceToNextFetch(POLLING_INTERVAL);
await waitForPromises();
expect(findLoadingIcon().exists()).toBe(false);
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(2);
+
+ advanceToNextFetch(POLLING_INTERVAL);
+ await waitForPromises();
+
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(2);
});
+ });
+
+ const testBehaviorWhenCacheIsPopulated = (queryResponse) => {
+ beforeEach(async () => {
+ mockCiConfigVariables.mockResolvedValue(queryResponse);
+ createComponentWithApollo({ method: mountExtended });
+ });
+
+ it('does not poll for new values', async () => {
+ await waitForPromises();
+
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(1);
+
+ advanceToNextFetch(POLLING_INTERVAL);
+ await waitForPromises();
+
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(1);
+ });
+
+ it('loading icon is shown when content is requested and hidden when received', async () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ };
+
+ describe('When no variables are defined in the CI configuration and the cache is updated', () => {
+ testBehaviorWhenCacheIsPopulated(mockEmptyCiConfigVariablesResponse);
+
+ it('displays an empty form', async () => {
+ mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
+ createComponentWithApollo({ method: mountExtended });
+ await waitForPromises();
+
+ expect(findKeyInputs().at(0).element.value).toBe('');
+ expect(findValueInputs().at(0).element.value).toBe('');
+ expect(findVariableTypes().at(0).props('text')).toBe('Variable');
+ });
+ });
+
+ describe('When CI configuration has defined variables and they are stored in the cache', () => {
+ testBehaviorWhenCacheIsPopulated(mockCiConfigVariablesResponse);
describe('with different predefined values', () => {
beforeEach(async () => {
diff --git a/spec/frontend/ci/pipeline_new/mock_data.js b/spec/frontend/ci/pipeline_new/mock_data.js
index dfb643a0ba4..5b935c0c819 100644
--- a/spec/frontend/ci/pipeline_new/mock_data.js
+++ b/spec/frontend/ci/pipeline_new/mock_data.js
@@ -132,3 +132,4 @@ export const mockEmptyCiConfigVariablesResponse = mockCiConfigVariablesQueryResp
export const mockCiConfigVariablesResponseWithoutDesc = mockCiConfigVariablesQueryResponse(
mockYamlVariablesWithoutDesc,
);
+export const mockNoCachedCiConfigVariablesResponse = mockCiConfigVariablesQueryResponse(null);
diff --git a/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
index 5ca4b25da9b..90ca2a07266 100644
--- a/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
@@ -38,20 +38,20 @@ describe('code quality issue body issue body', () => {
describe('severity rating', () => {
it.each`
severity | iconClass | iconName
- ${'INFO'} | ${'text-primary-400'} | ${'severity-info'}
- ${'MINOR'} | ${'text-warning-200'} | ${'severity-low'}
- ${'CRITICAL'} | ${'text-danger-600'} | ${'severity-high'}
- ${'BLOCKER'} | ${'text-danger-800'} | ${'severity-critical'}
- ${'UNKNOWN'} | ${'text-secondary-400'} | ${'severity-unknown'}
- ${'INVALID'} | ${'text-secondary-400'} | ${'severity-unknown'}
- ${'info'} | ${'text-primary-400'} | ${'severity-info'}
- ${'minor'} | ${'text-warning-200'} | ${'severity-low'}
- ${'major'} | ${'text-warning-400'} | ${'severity-medium'}
- ${'critical'} | ${'text-danger-600'} | ${'severity-high'}
- ${'blocker'} | ${'text-danger-800'} | ${'severity-critical'}
- ${'unknown'} | ${'text-secondary-400'} | ${'severity-unknown'}
- ${'invalid'} | ${'text-secondary-400'} | ${'severity-unknown'}
- ${undefined} | ${'text-secondary-400'} | ${'severity-unknown'}
+ ${'INFO'} | ${'gl-text-blue-400'} | ${'severity-info'}
+ ${'MINOR'} | ${'gl-text-orange-200'} | ${'severity-low'}
+ ${'CRITICAL'} | ${'gl-text-red-600'} | ${'severity-high'}
+ ${'BLOCKER'} | ${'gl-text-red-800'} | ${'severity-critical'}
+ ${'UNKNOWN'} | ${'gl-text-gray-400'} | ${'severity-unknown'}
+ ${'INVALID'} | ${'gl-text-gray-400'} | ${'severity-unknown'}
+ ${'info'} | ${'gl-text-blue-400'} | ${'severity-info'}
+ ${'minor'} | ${'gl-text-orange-200'} | ${'severity-low'}
+ ${'major'} | ${'gl-text-orange-400'} | ${'severity-medium'}
+ ${'critical'} | ${'gl-text-red-600'} | ${'severity-high'}
+ ${'blocker'} | ${'gl-text-red-800'} | ${'severity-critical'}
+ ${'unknown'} | ${'gl-text-gray-400'} | ${'severity-unknown'}
+ ${'invalid'} | ${'gl-text-gray-400'} | ${'severity-unknown'}
+ ${undefined} | ${'gl-text-gray-400'} | ${'severity-unknown'}
`(
'renders correct icon for "$severity" severity rating',
({ severity, iconClass, iconName }) => {
diff --git a/spec/frontend/ci/reports/codequality_report/store/actions_spec.js b/spec/frontend/ci/reports/codequality_report/store/actions_spec.js
index 88628210793..a606bce3d78 100644
--- a/spec/frontend/ci/reports/codequality_report/store/actions_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/store/actions_spec.js
@@ -2,6 +2,11 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
+import {
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
import createStore from '~/ci/reports/codequality_report/store';
import * as actions from '~/ci/reports/codequality_report/store/actions';
import * as types from '~/ci/reports/codequality_report/store/mutation_types';
@@ -55,7 +60,7 @@ describe('Codequality Reports actions', () => {
describe('on success', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', () => {
- mock.onGet(endpoint).reply(200, reportIssues);
+ mock.onGet(endpoint).reply(HTTP_STATUS_OK, reportIssues);
return testAction(
actions.fetchReports,
@@ -74,7 +79,7 @@ describe('Codequality Reports actions', () => {
describe('on error', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsError', () => {
- mock.onGet(endpoint).reply(500);
+ mock.onGet(endpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return testAction(
actions.fetchReports,
@@ -89,7 +94,7 @@ describe('Codequality Reports actions', () => {
describe('when base report is not found', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsError', () => {
const data = { status: STATUS_NOT_FOUND };
- mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(200, data);
+ mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(HTTP_STATUS_OK, data);
return testAction(
actions.fetchReports,
@@ -105,9 +110,9 @@ describe('Codequality Reports actions', () => {
it('continues polling until it receives data', () => {
mock
.onGet(endpoint)
- .replyOnce(204, undefined, pollIntervalHeader)
+ .replyOnce(HTTP_STATUS_NO_CONTENT, undefined, pollIntervalHeader)
.onGet(endpoint)
- .reply(200, reportIssues);
+ .reply(HTTP_STATUS_OK, reportIssues);
return Promise.all([
testAction(
@@ -134,9 +139,9 @@ describe('Codequality Reports actions', () => {
it('continues polling until it receives an error', () => {
mock
.onGet(endpoint)
- .replyOnce(204, undefined, pollIntervalHeader)
+ .replyOnce(HTTP_STATUS_NO_CONTENT, undefined, pollIntervalHeader)
.onGet(endpoint)
- .reply(500);
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Promise.all([
testAction(
diff --git a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
new file mode 100644
index 00000000000..edf3d1706cc
--- /dev/null
+++ b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
@@ -0,0 +1,80 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+
+import AdminNewRunnerApp from '~/ci/runner/admin_new_runner/admin_new_runner_app.vue';
+import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
+import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue';
+import { DEFAULT_PLATFORM } from '~/ci/runner/constants';
+
+const mockLegacyRegistrationToken = 'LEGACY_REGISTRATION_TOKEN';
+
+Vue.use(VueApollo);
+
+describe('AdminNewRunnerApp', () => {
+ let wrapper;
+
+ const findLegacyInstructionsLink = () => wrapper.findByTestId('legacy-instructions-link');
+ const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
+ const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup);
+ const findRunnerFormFields = () => wrapper.findComponent(RunnerFormFields);
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
+ wrapper = mountFn(AdminNewRunnerApp, {
+ propsData: {
+ legacyRegistrationToken: mockLegacyRegistrationToken,
+ ...props,
+ },
+ directives: {
+ GlModal: createMockDirective(),
+ },
+ stubs: {
+ GlSprintf,
+ },
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('Shows legacy modal', () => {
+ it('passes legacy registration to modal', () => {
+ expect(findRunnerInstructionsModal().props('registrationToken')).toEqual(
+ mockLegacyRegistrationToken,
+ );
+ });
+
+ it('opens a modal with the legacy instructions', () => {
+ const modalId = getBinding(findLegacyInstructionsLink().element, 'gl-modal').value;
+
+ expect(findRunnerInstructionsModal().props('modalId')).toBe(modalId);
+ });
+ });
+
+ describe('New runner form fields', () => {
+ describe('Platform', () => {
+ it('shows the platforms radio group', () => {
+ expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM);
+ });
+ });
+
+ describe('Runner', () => {
+ it('shows the runners fields', () => {
+ expect(findRunnerFormFields().props('value')).toEqual({
+ accessLevel: 'NOT_PROTECTED',
+ paused: false,
+ description: '',
+ maintenanceNote: '',
+ maximumTimeout: ' ',
+ runUntagged: false,
+ tagList: '',
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
index e233268b756..ed4f43c12d8 100644
--- a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -1,8 +1,6 @@
import Vue from 'vue';
-import { GlTab, GlTabs } from '@gitlab/ui';
import VueRouter from 'vue-router';
import VueApollo from 'vue-apollo';
-import setWindowLocation from 'helpers/set_window_location_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -15,6 +13,7 @@ import RunnerDetails from '~/ci/runner/components/runner_details.vue';
import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
+import RunnerDetailsTabs from '~/ci/runner/components/runner_details_tabs.vue';
import RunnersJobs from '~/ci/runner/components/runner_jobs.vue';
import runnerQuery from '~/ci/runner/graphql/show/runner.query.graphql';
@@ -42,14 +41,12 @@ describe('AdminRunnerShowApp', () => {
let mockRunnerQuery;
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
- const findTabs = () => wrapper.findComponent(GlTabs);
- const findTabAt = (i) => wrapper.findAllComponents(GlTab).at(i);
const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton);
const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
+ const findRunnerDetailsTabs = () => wrapper.findComponent(RunnerDetailsTabs);
const findRunnersJobs = () => wrapper.findComponent(RunnersJobs);
- const findJobCountBadge = () => wrapper.findByTestId('job-count-badge');
const mockRunnerQueryResult = (runner = {}) => {
mockRunnerQuery = jest.fn().mockResolvedValue({
@@ -89,16 +86,20 @@ describe('AdminRunnerShowApp', () => {
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId });
});
- it('displays the runner header', async () => {
+ it('displays the runner header', () => {
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
});
it('displays the runner edit and pause buttons', async () => {
- expect(findRunnerEditButton().exists()).toBe(true);
+ expect(findRunnerEditButton().attributes('href')).toBe(mockRunner.editAdminUrl);
expect(findRunnerPauseButton().exists()).toBe(true);
expect(findRunnerDeleteButton().exists()).toBe(true);
});
+ it('shows runner details', () => {
+ expect(findRunnerDetailsTabs().props('runner')).toEqual(mockRunner);
+ });
+
it('shows basic runner details', async () => {
const expected = `Description My Runner
Last contact Never contacted
@@ -118,20 +119,11 @@ describe('AdminRunnerShowApp', () => {
expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
});
- it.each(['#/', '#/unknown-tab'])('shows details when location hash is `%s`', async (hash) => {
- setWindowLocation(hash);
-
- await createComponent({ mountFn: mountExtended });
-
- expect(findTabs().props('value')).toBe(0);
- expect(findRunnerDetails().exists()).toBe(true);
- expect(findRunnersJobs().exists()).toBe(false);
- });
-
describe('when runner cannot be updated', () => {
beforeEach(async () => {
mockRunnerQueryResult({
userPermissions: {
+ ...mockRunner.userPermissions,
updateRunner: false,
},
});
@@ -145,12 +137,17 @@ describe('AdminRunnerShowApp', () => {
expect(findRunnerEditButton().exists()).toBe(false);
expect(findRunnerPauseButton().exists()).toBe(false);
});
+
+ it('displays delete button', () => {
+ expect(findRunnerDeleteButton().exists()).toBe(true);
+ });
});
describe('when runner cannot be deleted', () => {
beforeEach(async () => {
mockRunnerQueryResult({
userPermissions: {
+ ...mockRunner.userPermissions,
deleteRunner: false,
},
});
@@ -160,9 +157,14 @@ describe('AdminRunnerShowApp', () => {
});
});
- it('does not display the runner edit and pause buttons', () => {
+ it('does not display the delete button', () => {
expect(findRunnerDeleteButton().exists()).toBe(false);
});
+
+ it('displays edit and pause buttons', () => {
+ expect(findRunnerEditButton().exists()).toBe(true);
+ expect(findRunnerPauseButton().exists()).toBe(true);
+ });
});
describe('when runner is deleted', () => {
@@ -240,74 +242,4 @@ describe('AdminRunnerShowApp', () => {
expect(createAlert).toHaveBeenCalled();
});
});
-
- describe('When showing jobs', () => {
- const stubs = {
- GlTab,
- GlTabs,
- };
-
- it('without a runner, shows no jobs', () => {
- mockRunnerQuery = jest.fn().mockResolvedValue({
- data: {
- runner: null,
- },
- });
-
- createComponent({ stubs });
-
- expect(findJobCountBadge().exists()).toBe(false);
- expect(findRunnersJobs().exists()).toBe(false);
- });
-
- it('when URL hash links to jobs tab', async () => {
- mockRunnerQueryResult();
- setWindowLocation('#/jobs');
-
- await createComponent({ mountFn: mountExtended });
-
- expect(findTabs().props('value')).toBe(1);
- expect(findRunnerDetails().exists()).toBe(false);
- expect(findRunnersJobs().exists()).toBe(true);
- });
-
- it('without a job count, shows no jobs count', async () => {
- mockRunnerQueryResult({ jobCount: null });
-
- await createComponent({ stubs });
-
- expect(findJobCountBadge().exists()).toBe(false);
- });
-
- it('with a job count, shows jobs count', async () => {
- const runner = { jobCount: 3 };
- mockRunnerQueryResult(runner);
-
- await createComponent({ stubs });
-
- expect(findJobCountBadge().text()).toBe('3');
- });
- });
-
- describe('When navigating to another tab', () => {
- let routerPush;
-
- beforeEach(async () => {
- mockRunnerQueryResult();
-
- await createComponent({ mountFn: mountExtended });
-
- routerPush = jest.spyOn(wrapper.vm.$router, 'push').mockImplementation(() => {});
- });
-
- it('navigates to details', () => {
- findTabAt(0).vm.$emit('click');
- expect(routerPush).toHaveBeenLastCalledWith({ name: 'details' });
- });
-
- it('navigates to job', () => {
- findTabAt(1).vm.$emit('click');
- expect(routerPush).toHaveBeenLastCalledWith({ name: 'jobs' });
- });
- });
});
diff --git a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
index 9084ecdb4cc..7fc240e520b 100644
--- a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
@@ -39,6 +39,7 @@ import {
I18N_GROUP_TYPE,
I18N_PROJECT_TYPE,
INSTANCE_TYPE,
+ JOBS_ROUTE_PATH,
PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
@@ -56,6 +57,7 @@ import {
allRunnersDataPaginated,
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ newRunnerPath,
emptyPageInfo,
emptyStateSvgPath,
emptyStateFilteredSvgPath,
@@ -113,6 +115,7 @@ describe('AdminRunnersApp', () => {
apolloProvider: createMockApollo(handlers, {}, cacheConfig),
propsData: {
registrationToken: mockRegistrationToken,
+ newRunnerPath,
...props,
},
provide: {
@@ -280,11 +283,14 @@ describe('AdminRunnersApp', () => {
it('Shows job status and links to jobs', () => {
const badge = wrapper
- .find('tr [data-testid="td-summary"]')
+ .find('tr [data-testid="td-status"]')
.findComponent(RunnerJobStatusBadge);
expect(badge.props('jobStatus')).toBe(mockRunners[0].jobExecutionStatus);
- expect(badge.attributes('href')).toBe(`http://localhost/admin/runners/${id}#/jobs`);
+
+ const badgeHref = new URL(badge.attributes('href'));
+ expect(badgeHref.pathname).toBe(`/admin/runners/${id}`);
+ expect(badgeHref.hash).toBe(`#${JOBS_ROUTE_PATH}`);
});
it('When runner is paused or unpaused, some data is refetched', async () => {
@@ -443,7 +449,13 @@ describe('AdminRunnersApp', () => {
});
it('shows an empty state', () => {
- expect(findRunnerListEmptyState().props('isSearchFiltered')).toBe(false);
+ expect(findRunnerListEmptyState().props()).toEqual({
+ newRunnerPath,
+ isSearchFiltered: false,
+ filteredSvgPath: emptyStateFilteredSvgPath,
+ registrationToken: mockRegistrationToken,
+ svgPath: emptyStateSvgPath,
+ });
});
describe('when a filter is selected by the user', () => {
diff --git a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
index 2fb824a8fa5..1ff60ff1a9d 100644
--- a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
@@ -10,6 +10,7 @@ import {
INSTANCE_TYPE,
STATUS_ONLINE,
STATUS_OFFLINE,
+ JOB_STATUS_IDLE,
} from '~/ci/runner/constants';
describe('RunnerStatusCell', () => {
@@ -18,16 +19,18 @@ describe('RunnerStatusCell', () => {
const findStatusBadge = () => wrapper.findComponent(RunnerStatusBadge);
const findPausedBadge = () => wrapper.findComponent(RunnerPausedBadge);
- const createComponent = ({ runner = {} } = {}) => {
+ const createComponent = ({ runner = {}, ...options } = {}) => {
wrapper = mount(RunnerStatusCell, {
propsData: {
runner: {
runnerType: INSTANCE_TYPE,
active: true,
status: STATUS_ONLINE,
+ jobExecutionStatus: JOB_STATUS_IDLE,
...runner,
},
},
+ ...options,
});
};
@@ -74,4 +77,14 @@ describe('RunnerStatusCell', () => {
expect(wrapper.text()).toBe('');
});
+
+ it('Displays "runner-job-status-badge" slot', () => {
+ createComponent({
+ scopedSlots: {
+ 'runner-job-status-badge': ({ runner }) => `Job status ${runner.jobExecutionStatus}`,
+ },
+ });
+
+ expect(wrapper.text()).toContain(`Job status ${JOB_STATUS_IDLE}`);
+ });
});
diff --git a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
index 10280c77303..1711df42491 100644
--- a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
@@ -3,7 +3,6 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import RunnerSummaryCell from '~/ci/runner/components/cells/runner_summary_cell.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import RunnerTags from '~/ci/runner/components/runner_tags.vue';
-import RunnerJobStatusBadge from '~/ci/runner/components/runner_job_status_badge.vue';
import RunnerSummaryField from '~/ci/runner/components/cells/runner_summary_field.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -22,7 +21,6 @@ describe('RunnerTypeCell', () => {
let wrapper;
const findLockIcon = () => wrapper.findByTestId('lock-icon');
- const findRunnerJobStatusBadge = () => wrapper.findComponent(RunnerJobStatusBadge);
const findRunnerTags = () => wrapper.findComponent(RunnerTags);
const findRunnerSummaryField = (icon) =>
wrapper.findAllComponents(RunnerSummaryField).filter((w) => w.props('icon') === icon)
@@ -95,10 +93,6 @@ describe('RunnerTypeCell', () => {
expect(wrapper.text()).toContain(I18N_NO_DESCRIPTION);
});
- it('Displays job execution status', () => {
- expect(findRunnerJobStatusBadge().props('jobStatus')).toBe(mockRunner.jobExecutionStatus);
- });
-
it('Displays last contact', () => {
createComponent({
contactedAt: '2022-01-02',
@@ -166,14 +160,14 @@ describe('RunnerTypeCell', () => {
expect(findRunnerTags().props('tagList')).toEqual(['shell', 'linux']);
});
- it.each(['runner-name', 'runner-job-status-badge'])('Displays a custom "%s" slot', (slotName) => {
+ it('Displays a custom runner-name slot', () => {
const slotContent = 'My custom runner name';
createComponent(
{},
{
slots: {
- [slotName]: slotContent,
+ 'runner-name': slotContent,
},
},
);
diff --git a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
index 0ecafdd7d83..0daaca9c4ff 100644
--- a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
@@ -1,8 +1,9 @@
import { GlModal, GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui';
import { mount, shallowMount, createWrapper } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
-
import VueApollo from 'vue-apollo';
+
+import { s__ } from '~/locale';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -84,9 +85,9 @@ describe('RegistrationDropdown', () => {
it.each`
type | text
- ${INSTANCE_TYPE} | ${'Register an instance runner'}
- ${GROUP_TYPE} | ${'Register a group runner'}
- ${PROJECT_TYPE} | ${'Register a project runner'}
+ ${INSTANCE_TYPE} | ${s__('Runners|Register an instance runner')}
+ ${GROUP_TYPE} | ${s__('Runners|Register a group runner')}
+ ${PROJECT_TYPE} | ${s__('Runners|Register a project runner')}
`('Dropdown text for type $type is "$text"', () => {
createComponent({ props: { type: INSTANCE_TYPE } }, mount);
diff --git a/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
index 64f5a0e3b57..0dc5a90fb83 100644
--- a/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
+++ b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { makeVar } from '@apollo/client/core';
import { GlModal, GlSprintf } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { createAlert } from '~/flash';
@@ -22,6 +23,7 @@ describe('RunnerBulkDelete', () => {
let mockState;
let mockCheckedRunnerIds;
+ const findBanner = () => wrapper.findByTestId('runner-bulk-delete-banner');
const findClearBtn = () => wrapper.findByText(s__('Runners|Clear selection'));
const findDeleteBtn = () => wrapper.findByText(s__('Runners|Delete selected'));
const findModal = () => wrapper.findComponent(GlModal);
@@ -64,10 +66,11 @@ describe('RunnerBulkDelete', () => {
beforeEach(() => {
mockState = createLocalState();
+ mockCheckedRunnerIds = makeVar([]);
jest
.spyOn(mockState.cacheConfig.typePolicies.Query.fields, 'checkedRunnerIds')
- .mockImplementation(() => mockCheckedRunnerIds);
+ .mockImplementation(() => mockCheckedRunnerIds());
});
afterEach(() => {
@@ -76,15 +79,13 @@ describe('RunnerBulkDelete', () => {
describe('When no runners are checked', () => {
beforeEach(async () => {
- mockCheckedRunnerIds = [];
-
createComponent();
await waitForPromises();
});
it('shows no contents', () => {
- expect(wrapper.html()).toBe('');
+ expect(findBanner().exists()).toBe(false);
});
});
@@ -94,7 +95,7 @@ describe('RunnerBulkDelete', () => {
${2} | ${[mockId1, mockId2]} | ${'2 runners'}
`('When $count runner(s) are checked', ({ ids, text }) => {
beforeEach(() => {
- mockCheckedRunnerIds = ids;
+ mockCheckedRunnerIds(ids);
createComponent();
@@ -102,7 +103,7 @@ describe('RunnerBulkDelete', () => {
});
it(`shows "${text}"`, () => {
- expect(wrapper.text()).toContain(text);
+ expect(findBanner().text()).toContain(text);
});
it('clears selection', () => {
@@ -133,7 +134,7 @@ describe('RunnerBulkDelete', () => {
};
beforeEach(() => {
- mockCheckedRunnerIds = [mockId1, mockId2];
+ mockCheckedRunnerIds([mockId1, mockId2]);
createComponent();
@@ -157,20 +158,23 @@ describe('RunnerBulkDelete', () => {
it('mutation is called', () => {
expect(bulkRunnerDeleteHandler).toHaveBeenCalledWith({
- input: { ids: mockCheckedRunnerIds },
+ input: { ids: mockCheckedRunnerIds() },
});
});
});
describe('when deletion is successful', () => {
+ let deletedIds;
+
beforeEach(async () => {
+ deletedIds = mockCheckedRunnerIds();
bulkRunnerDeleteHandler.mockResolvedValue({
data: {
- bulkRunnerDelete: { deletedIds: mockCheckedRunnerIds, errors: [] },
+ bulkRunnerDelete: { deletedIds, errors: [] },
},
});
-
confirmDeletion();
+ mockCheckedRunnerIds([]);
await waitForPromises();
});
@@ -182,12 +186,12 @@ describe('RunnerBulkDelete', () => {
it('user interface is updated', () => {
const { evict, gc } = apolloCache;
- expect(evict).toHaveBeenCalledTimes(mockCheckedRunnerIds.length);
+ expect(evict).toHaveBeenCalledTimes(deletedIds.length);
expect(evict).toHaveBeenCalledWith({
- id: expect.stringContaining(mockCheckedRunnerIds[0]),
+ id: expect.stringContaining(deletedIds[0]),
});
expect(evict).toHaveBeenCalledWith({
- id: expect.stringContaining(mockCheckedRunnerIds[1]),
+ id: expect.stringContaining(deletedIds[1]),
});
expect(gc).toHaveBeenCalledTimes(1);
@@ -195,7 +199,7 @@ describe('RunnerBulkDelete', () => {
it('emits deletion confirmation', () => {
expect(wrapper.emitted('deleted')).toEqual([
- [{ message: expect.stringContaining(`${mockCheckedRunnerIds.length}`) }],
+ [{ message: expect.stringContaining(`${deletedIds.length}`) }],
]);
});
diff --git a/spec/frontend/ci/runner/components/runner_details_tabs_spec.js b/spec/frontend/ci/runner/components/runner_details_tabs_spec.js
new file mode 100644
index 00000000000..a59c5a21377
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_details_tabs_spec.js
@@ -0,0 +1,127 @@
+import Vue from 'vue';
+import { GlTab, GlTabs } from '@gitlab/ui';
+import VueRouter from 'vue-router';
+import VueApollo from 'vue-apollo';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { JOBS_ROUTE_PATH, I18N_DETAILS, I18N_JOBS } from '~/ci/runner/constants';
+
+import RunnerDetailsTabs from '~/ci/runner/components/runner_details_tabs.vue';
+import RunnerDetails from '~/ci/runner/components/runner_details.vue';
+import RunnerJobs from '~/ci/runner/components/runner_jobs.vue';
+
+import { runnerData } from '../mock_data';
+
+// Vue Test Utils `stubs` option does not stub components mounted
+// in <router-view>. Use mocking instead:
+jest.mock('~/ci/runner/components/runner_jobs.vue', () => {
+ const ActualRunnerJobs = jest.requireActual('~/ci/runner/components/runner_jobs.vue').default;
+ return {
+ props: ActualRunnerJobs.props,
+ render() {},
+ };
+});
+
+const mockRunner = runnerData.data.runner;
+
+Vue.use(VueApollo);
+Vue.use(VueRouter);
+
+describe('RunnerDetailsTabs', () => {
+ let wrapper;
+ let routerPush;
+
+ const findTabs = () => wrapper.findComponent(GlTabs);
+ const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
+ const findRunnerJobs = () => wrapper.findComponent(RunnerJobs);
+ const findJobCountBadge = () => wrapper.findByTestId('job-count-badge');
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
+ wrapper = mountFn(RunnerDetailsTabs, {
+ propsData: {
+ runner: mockRunner,
+ ...props,
+ },
+ ...options,
+ });
+
+ routerPush = jest.spyOn(wrapper.vm.$router, 'push').mockImplementation(() => {});
+
+ return waitForPromises();
+ };
+
+ it('shows basic runner details', async () => {
+ await createComponent({ mountFn: mountExtended });
+
+ expect(findRunnerDetails().props('runner')).toBe(mockRunner);
+ expect(findRunnerJobs().exists()).toBe(false);
+ });
+
+ it('shows runner jobs', async () => {
+ setWindowLocation(`#${JOBS_ROUTE_PATH}`);
+
+ await createComponent({ mountFn: mountExtended });
+
+ expect(findRunnerDetails().exists()).toBe(false);
+ expect(findRunnerJobs().props('runner')).toBe(mockRunner);
+ });
+
+ it.each`
+ jobCount | badgeText
+ ${null} | ${null}
+ ${1} | ${'1'}
+ ${1000} | ${'1,000'}
+ ${1001} | ${'1,000+'}
+ `('shows runner jobs count', async ({ jobCount, badgeText }) => {
+ await createComponent({
+ stubs: {
+ GlTab,
+ },
+ props: {
+ runner: {
+ ...mockRunner,
+ jobCount,
+ },
+ },
+ });
+
+ if (!badgeText) {
+ expect(findJobCountBadge().exists()).toBe(false);
+ } else {
+ expect(findJobCountBadge().text()).toBe(badgeText);
+ }
+ });
+
+ it.each(['#/', '#/unknown-tab'])('shows details when location hash is `%s`', async (hash) => {
+ setWindowLocation(hash);
+
+ await createComponent({ mountFn: mountExtended });
+
+ expect(findTabs().props('value')).toBe(0);
+ expect(findRunnerDetails().exists()).toBe(true);
+ expect(findRunnerJobs().exists()).toBe(false);
+ });
+
+ describe.each`
+ location | tab | navigatedTo
+ ${'#/details'} | ${I18N_DETAILS} | ${[]}
+ ${'#/details'} | ${I18N_JOBS} | ${[[{ name: 'jobs' }]]}
+ ${'#/jobs'} | ${I18N_JOBS} | ${[]}
+ ${'#/jobs'} | ${I18N_DETAILS} | ${[[{ name: 'details' }]]}
+ `('When at $location', ({ location, tab, navigatedTo }) => {
+ beforeEach(async () => {
+ setWindowLocation(location);
+
+ await createComponent({
+ mountFn: mountExtended,
+ });
+ });
+
+ it(`on click on ${tab}, navigates to ${JSON.stringify(navigatedTo)}`, () => {
+ wrapper.findByText(tab).trigger('click');
+
+ expect(routerPush.mock.calls).toEqual(navigatedTo);
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_form_fields_spec.js b/spec/frontend/ci/runner/components/runner_form_fields_spec.js
new file mode 100644
index 00000000000..5b429645d17
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_form_fields_spec.js
@@ -0,0 +1,87 @@
+import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue';
+import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED } from '~/ci/runner/constants';
+
+const mockDescription = 'My description';
+const mockMaxTimeout = 60;
+const mockTags = 'tag, tag2';
+
+describe('RunnerFormFields', () => {
+ let wrapper;
+
+ const findInput = (name) => wrapper.find(`input[name="${name}"]`);
+
+ const createComponent = ({ runner } = {}) => {
+ wrapper = mountExtended(RunnerFormFields, {
+ propsData: {
+ value: runner,
+ },
+ });
+ };
+
+ it('updates runner fields', async () => {
+ createComponent();
+
+ expect(wrapper.emitted('input')).toBe(undefined);
+
+ findInput('description').setValue(mockDescription);
+ findInput('max-timeout').setValue(mockMaxTimeout);
+ findInput('paused').setChecked(true);
+ findInput('protected').setChecked(true);
+ findInput('run-untagged').setChecked(true);
+ findInput('tags').setValue(mockTags);
+
+ await nextTick();
+
+ expect(wrapper.emitted('input')[0][0]).toMatchObject({
+ description: mockDescription,
+ maximumTimeout: mockMaxTimeout,
+ tagList: mockTags,
+ });
+ });
+
+ it('checks checkbox fields', async () => {
+ createComponent({
+ runner: {
+ paused: false,
+ accessLevel: ACCESS_LEVEL_NOT_PROTECTED,
+ runUntagged: false,
+ },
+ });
+
+ findInput('paused').setChecked(true);
+ findInput('protected').setChecked(true);
+ findInput('run-untagged').setChecked(true);
+
+ await nextTick();
+
+ expect(wrapper.emitted('input')[0][0]).toEqual({
+ paused: true,
+ accessLevel: ACCESS_LEVEL_REF_PROTECTED,
+ runUntagged: true,
+ });
+ });
+
+ it('unchecks checkbox fields', async () => {
+ createComponent({
+ runner: {
+ paused: true,
+ accessLevel: ACCESS_LEVEL_REF_PROTECTED,
+ runUntagged: true,
+ },
+ });
+
+ findInput('paused').setChecked(false);
+ findInput('protected').setChecked(false);
+ findInput('run-untagged').setChecked(false);
+
+ await nextTick();
+
+ expect(wrapper.emitted('input')[0][0]).toEqual({
+ paused: false,
+ accessLevel: ACCESS_LEVEL_NOT_PROTECTED,
+ runUntagged: false,
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_header_spec.js b/spec/frontend/ci/runner/components/runner_header_spec.js
index a04011de1cd..abe3b47767e 100644
--- a/spec/frontend/ci/runner/components/runner_header_spec.js
+++ b/spec/frontend/ci/runner/components/runner_header_spec.js
@@ -6,7 +6,7 @@ import {
GROUP_TYPE,
STATUS_ONLINE,
} from '~/ci/runner/constants';
-import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
+import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -71,7 +71,7 @@ describe('RunnerHeader', () => {
it('displays the runner id', () => {
createComponent({
runner: {
- id: convertToGraphQLId(TYPE_CI_RUNNER, 99),
+ id: convertToGraphQLId(TYPENAME_CI_RUNNER, 99),
},
});
@@ -99,7 +99,7 @@ describe('RunnerHeader', () => {
it('does not display runner creation time if "createdAt" is missing', () => {
createComponent({
runner: {
- id: convertToGraphQLId(TYPE_CI_RUNNER, 99),
+ id: convertToGraphQLId(TYPENAME_CI_RUNNER, 99),
createdAt: null,
},
});
diff --git a/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js b/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js
index 015bebf40e3..c4476d01386 100644
--- a/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js
@@ -23,16 +23,25 @@ describe('RunnerTypeBadge', () => {
};
it.each`
- jobStatus | classes | text
- ${JOB_STATUS_RUNNING} | ${['gl-mr-3', 'gl-bg-transparent!', 'gl-text-blue-600!', 'gl-border', 'gl-border-blue-600!']} | ${I18N_JOB_STATUS_RUNNING}
- ${JOB_STATUS_IDLE} | ${['gl-mr-3', 'gl-bg-transparent!', 'gl-text-gray-700!', 'gl-border', 'gl-border-gray-500!']} | ${I18N_JOB_STATUS_IDLE}
+ jobStatus | classes | text
+ ${JOB_STATUS_RUNNING} | ${['gl-text-blue-600!', 'gl-border-blue-600!']} | ${I18N_JOB_STATUS_RUNNING}
+ ${JOB_STATUS_IDLE} | ${['gl-text-gray-700!', 'gl-border-gray-500!']} | ${I18N_JOB_STATUS_IDLE}
`(
'renders $jobStatus job status with "$text" text and styles',
({ jobStatus, classes, text }) => {
createComponent({ props: { jobStatus } });
- expect(findBadge().props()).toMatchObject({ size: 'sm', variant: 'muted' });
- expect(findBadge().classes().sort()).toEqual(classes.sort());
+ expect(findBadge().props()).toMatchObject({ size: 'md', variant: 'muted' });
+ expect(findBadge().classes().sort()).toEqual(
+ [
+ ...classes,
+ 'gl-border',
+ 'gl-display-inline-block',
+ 'gl-max-w-full',
+ 'gl-text-truncate',
+ 'gl-bg-transparent!',
+ ].sort(),
+ );
expect(findBadge().text()).toBe(text);
},
);
diff --git a/spec/frontend/ci/runner/components/runner_jobs_table_spec.js b/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
index 8defe568df8..281aa1aeb77 100644
--- a/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
+++ b/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
@@ -72,7 +72,7 @@ describe('RunnerJobsTable', () => {
});
it('Displays details of a job', () => {
- const { id, detailedStatus, pipeline, shortSha, commitPath } = mockJobs[0];
+ const { id, detailedStatus, project, shortSha, commitPath } = mockJobs[0];
expect(findCell({ field: 'status' }).text()).toMatchInterpolatedText(detailedStatus.text);
@@ -81,10 +81,8 @@ describe('RunnerJobsTable', () => {
detailedStatus.detailsPath,
);
- expect(findCell({ field: 'project' }).text()).toBe(pipeline.project.name);
- expect(findCell({ field: 'project' }).find('a').attributes('href')).toBe(
- pipeline.project.webUrl,
- );
+ expect(findCell({ field: 'project' }).text()).toBe(project.name);
+ expect(findCell({ field: 'project' }).find('a').attributes('href')).toBe(project.webUrl);
expect(findCell({ field: 'commit' }).text()).toBe(shortSha);
expect(findCell({ field: 'commit' }).find('a').attributes('href')).toBe(commitPath);
diff --git a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
index d351f7b6908..6aea3ddf58c 100644
--- a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
@@ -4,10 +4,14 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+import {
+ newRunnerPath,
+ emptyStateSvgPath,
+ emptyStateFilteredSvgPath,
+} from 'jest/ci/runner/mock_data';
+
import RunnerListEmptyState from '~/ci/runner/components/runner_list_empty_state.vue';
-const mockSvgPath = 'mock-svg-path.svg';
-const mockFilteredSvgPath = 'mock-filtered-svg-path.svg';
const mockRegistrationToken = 'REGISTRATION_TOKEN';
describe('RunnerListEmptyState', () => {
@@ -17,12 +21,13 @@ describe('RunnerListEmptyState', () => {
const findLink = () => wrapper.findComponent(GlLink);
const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
- const createComponent = ({ props, mountFn = shallowMountExtended } = {}) => {
+ const createComponent = ({ props, mountFn = shallowMountExtended, ...options } = {}) => {
wrapper = mountFn(RunnerListEmptyState, {
propsData: {
- svgPath: mockSvgPath,
- filteredSvgPath: mockFilteredSvgPath,
+ svgPath: emptyStateSvgPath,
+ filteredSvgPath: emptyStateFilteredSvgPath,
registrationToken: mockRegistrationToken,
+ newRunnerPath,
...props,
},
directives: {
@@ -33,6 +38,7 @@ describe('RunnerListEmptyState', () => {
GlSprintf,
GlLink,
},
+ ...options,
});
};
@@ -45,7 +51,7 @@ describe('RunnerListEmptyState', () => {
});
it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(mockSvgPath);
+ expect(findEmptyState().props('svgPath')).toBe(emptyStateSvgPath);
});
it('displays "no results" text with instructions', () => {
@@ -56,10 +62,53 @@ describe('RunnerListEmptyState', () => {
expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`);
});
- it('opens a runner registration instructions modal with a link', () => {
- const { value } = getBinding(findLink().element, 'gl-modal');
+ describe('when create_runner_workflow is enabled', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: { createRunnerWorkflow: true },
+ },
+ });
+ });
+
+ it('shows a link to the new runner page', () => {
+ expect(findLink().attributes('href')).toBe(newRunnerPath);
+ });
+ });
+
+ describe('when create_runner_workflow is enabled and newRunnerPath not defined', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ newRunnerPath: null,
+ },
+ provide: {
+ glFeatures: { createRunnerWorkflow: true },
+ },
+ });
+ });
+
+ it('opens a runner registration instructions modal with a link', () => {
+ const { value } = getBinding(findLink().element, 'gl-modal');
+
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ });
+ });
+
+ describe('when create_runner_workflow is disabled', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: { createRunnerWorkflow: false },
+ },
+ });
+ });
+
+ it('opens a runner registration instructions modal with a link', () => {
+ const { value } = getBinding(findLink().element, 'gl-modal');
- expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ });
});
});
@@ -69,7 +118,7 @@ describe('RunnerListEmptyState', () => {
});
it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(mockSvgPath);
+ expect(findEmptyState().props('svgPath')).toBe(emptyStateSvgPath);
});
it('displays "no results" text', () => {
@@ -92,7 +141,7 @@ describe('RunnerListEmptyState', () => {
});
it('renders a "filtered search" illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(mockFilteredSvgPath);
+ expect(findEmptyState().props('svgPath')).toBe(emptyStateFilteredSvgPath);
});
it('displays "no filtered results" text', () => {
diff --git a/spec/frontend/ci/runner/components/runner_list_spec.js b/spec/frontend/ci/runner/components/runner_list_spec.js
index 1267d045623..2e5d1dbd063 100644
--- a/spec/frontend/ci/runner/components/runner_list_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_spec.js
@@ -177,30 +177,30 @@ describe('RunnerList', () => {
});
describe('Scoped cell slots', () => {
- it('Render #runner-name slot in "summary" cell', () => {
+ it('Render #runner-job-status-badge slot in "status" cell', () => {
createComponent(
{
- scopedSlots: { 'runner-name': ({ runner }) => `Summary: ${runner.id}` },
+ scopedSlots: {
+ 'runner-job-status-badge': ({ runner }) => `Job status ${runner.jobExecutionStatus}`,
+ },
},
mountExtended,
);
- expect(findCell({ fieldKey: 'summary' }).text()).toContain(`Summary: ${mockRunners[0].id}`);
+ expect(findCell({ fieldKey: 'status' }).text()).toContain(
+ `Job status ${mockRunners[0].jobExecutionStatus}`,
+ );
});
- it('Render #runner-job-status-badge slot in "summary" cell', () => {
+ it('Render #runner-name slot in "summary" cell', () => {
createComponent(
{
- scopedSlots: {
- 'runner-job-status-badge': ({ runner }) => `Job status ${runner.jobExecutionStatus}`,
- },
+ scopedSlots: { 'runner-name': ({ runner }) => `Summary: ${runner.id}` },
},
mountExtended,
);
- expect(findCell({ fieldKey: 'summary' }).text()).toContain(
- `Job status ${mockRunners[0].jobExecutionStatus}`,
- );
+ expect(findCell({ fieldKey: 'summary' }).text()).toContain(`Summary: ${mockRunners[0].id}`);
});
it('Render #runner-actions-cell slot in "actions" cell', () => {
diff --git a/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js b/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js
new file mode 100644
index 00000000000..db6fd2c369b
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js
@@ -0,0 +1,96 @@
+import { nextTick } from 'vue';
+import { GlFormRadioGroup, GlIcon, GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import RunnerPlatformsRadio from '~/ci/runner/components/runner_platforms_radio.vue';
+import {
+ LINUX_PLATFORM,
+ MACOS_PLATFORM,
+ WINDOWS_PLATFORM,
+ AWS_PLATFORM,
+ DOCKER_HELP_URL,
+ KUBERNETES_HELP_URL,
+} from '~/ci/runner/constants';
+
+import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
+
+const mockProvide = {
+ awsImgPath: 'awsLogo.svg',
+ dockerImgPath: 'dockerLogo.svg',
+ kubernetesImgPath: 'kubernetesLogo.svg',
+};
+
+describe('RunnerPlatformsRadioGroup', () => {
+ let wrapper;
+
+ const findFormRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
+ const findFormRadios = () => wrapper.findAllComponents(RunnerPlatformsRadio).wrappers;
+ const findFormRadioByText = (text) =>
+ findFormRadios()
+ .filter((w) => w.text() === text)
+ .at(0);
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
+ wrapper = mountFn(RunnerPlatformsRadioGroup, {
+ propsData: {
+ value: null,
+ ...props,
+ },
+ provide: mockProvide,
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('contains expected options with images', () => {
+ const labels = findFormRadios().map((w) => [w.text(), w.props('image')]);
+
+ expect(labels).toEqual([
+ ['Linux', null],
+ ['macOS', null],
+ ['Windows', null],
+ ['AWS', expect.any(String)],
+ ['Docker', expect.any(String)],
+ ['Kubernetes', expect.any(String)],
+ ]);
+ });
+
+ it('allows users to use radio group', async () => {
+ findFormRadioGroup().vm.$emit('input', MACOS_PLATFORM);
+ await nextTick();
+
+ expect(wrapper.emitted('input')[0]).toEqual([MACOS_PLATFORM]);
+ });
+
+ it.each`
+ text | value
+ ${'Linux'} | ${LINUX_PLATFORM}
+ ${'macOS'} | ${MACOS_PLATFORM}
+ ${'Windows'} | ${WINDOWS_PLATFORM}
+ ${'AWS'} | ${AWS_PLATFORM}
+ `('user can select "$text"', async ({ text, value }) => {
+ const radio = findFormRadioByText(text);
+ expect(radio.props('value')).toBe(value);
+
+ radio.vm.$emit('input', value);
+ await nextTick();
+
+ expect(wrapper.emitted('input')[0]).toEqual([value]);
+ });
+
+ it.each`
+ text | href
+ ${'Docker'} | ${DOCKER_HELP_URL}
+ ${'Kubernetes'} | ${KUBERNETES_HELP_URL}
+ `('provides link to "$text" docs', async ({ text, href }) => {
+ const radio = findFormRadioByText(text);
+
+ expect(radio.findComponent(GlLink).attributes()).toEqual({
+ href,
+ target: '_blank',
+ });
+ expect(radio.findComponent(GlIcon).props('name')).toBe('external-link');
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js b/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js
new file mode 100644
index 00000000000..fb81edd1ae2
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js
@@ -0,0 +1,154 @@
+import { GlFormRadio } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import RunnerPlatformsRadio from '~/ci/runner/components/runner_platforms_radio.vue';
+
+const mockImg = 'mock.svg';
+const mockValue = 'value';
+const mockValue2 = 'value2';
+const mockSlot = '<div>a</div>';
+
+describe('RunnerPlatformsRadio', () => {
+ let wrapper;
+
+ const findDiv = () => wrapper.find('div');
+ const findImg = () => wrapper.find('img');
+ const findFormRadio = () => wrapper.findComponent(GlFormRadio);
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
+ wrapper = mountFn(RunnerPlatformsRadio, {
+ propsData: {
+ image: mockImg,
+ value: mockValue,
+ ...props,
+ },
+ ...options,
+ });
+ };
+
+ describe('when its selectable', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { value: mockValue },
+ });
+ });
+
+ it('shows the item is clickable', () => {
+ expect(wrapper.classes('gl-cursor-pointer')).toBe(true);
+ });
+
+ it('shows radio option', () => {
+ expect(findFormRadio().attributes('value')).toBe(mockValue);
+ });
+
+ it('emits when item is clicked', async () => {
+ findDiv().trigger('click');
+
+ expect(wrapper.emitted('input')).toEqual([[mockValue]]);
+ });
+
+ it.each(['input', 'change'])('emits radio "%s" event', (event) => {
+ findFormRadio().vm.$emit(event, mockValue2);
+
+ expect(wrapper.emitted(event)).toEqual([[mockValue2]]);
+ });
+
+ it('shows image', () => {
+ expect(findImg().attributes()).toMatchObject({
+ src: mockImg,
+ 'aria-hidden': 'true',
+ });
+ });
+
+ it('shows slot', () => {
+ createComponent({
+ slots: {
+ default: mockSlot,
+ },
+ });
+
+ expect(wrapper.html()).toContain(mockSlot);
+ });
+
+ describe('with no image', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { value: mockValue, image: null },
+ });
+ });
+
+ it('shows no image', () => {
+ expect(findImg().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('when its not selectable', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { value: null },
+ });
+ });
+
+ it('shows the item is clickable', () => {
+ expect(wrapper.classes('gl-cursor-pointer')).toBe(false);
+ });
+
+ it('does not emit when item is clicked', async () => {
+ findDiv().trigger('click');
+
+ expect(wrapper.emitted('input')).toBe(undefined);
+ });
+
+ it('does not show a radio option', () => {
+ expect(findFormRadio().exists()).toBe(false);
+ });
+
+ it('shows image', () => {
+ expect(findImg().attributes()).toMatchObject({
+ src: mockImg,
+ 'aria-hidden': 'true',
+ });
+ });
+
+ it('shows slot', () => {
+ createComponent({
+ slots: {
+ default: mockSlot,
+ },
+ });
+
+ expect(wrapper.html()).toContain(mockSlot);
+ });
+
+ describe('with no image', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { value: null, image: null },
+ });
+ });
+
+ it('shows no image', () => {
+ expect(findImg().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('when selected', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { checked: mockValue },
+ });
+ });
+
+ it('highlights the item', () => {
+ expect(wrapper.classes('gl-bg-blue-50')).toBe(true);
+ expect(wrapper.classes('gl-border-blue-500')).toBe(true);
+ });
+
+ it('shows radio option as selected', () => {
+ expect(findFormRadio().attributes('value')).toBe(mockValue);
+ expect(findFormRadio().props('checked')).toBe(mockValue);
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
index 3dce5a509ca..b7d9d3ad23e 100644
--- a/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
+++ b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
@@ -5,7 +5,7 @@ import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import TagToken, { TAG_SUGGESTIONS_PATH } from '~/ci/runner/components/search_tokens/tag_token.vue';
import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import { getRecentlyUsedSuggestions } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
@@ -80,10 +80,10 @@ describe('TagToken', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } }).reply(200, mockTags);
+ mock.onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } }).reply(HTTP_STATUS_OK, mockTags);
mock
.onGet(TAG_SUGGESTIONS_PATH, { params: { search: mockSearchTerm } })
- .reply(200, mockTagsFiltered);
+ .reply(HTTP_STATUS_OK, mockTagsFiltered);
getRecentlyUsedSuggestions.mockReturnValue([]);
});
@@ -163,7 +163,9 @@ describe('TagToken', () => {
describe('when suggestions cannot be loaded', () => {
beforeEach(async () => {
- mock.onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } }).reply(500);
+ mock
+ .onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } })
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent();
await waitForPromises();
diff --git a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
index c6c3f3b7040..2ad31dea774 100644
--- a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import VueRouter from 'vue-router';
import VueApollo from 'vue-apollo';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -12,6 +13,9 @@ import RunnerDetails from '~/ci/runner/components/runner_details.vue';
import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
+import RunnerDetailsTabs from '~/ci/runner/components/runner_details_tabs.vue';
+import RunnersJobs from '~/ci/runner/components/runner_jobs.vue';
+
import runnerQuery from '~/ci/runner/graphql/show/runner.query.graphql';
import GroupRunnerShowApp from '~/ci/runner/group_runner_show/group_runner_show_app.vue';
import { captureException } from '~/ci/runner/sentry_utils';
@@ -31,6 +35,7 @@ const mockRunnersPath = '/groups/group1/-/runners';
const mockEditGroupRunnerPath = `/groups/group1/-/runners/${mockRunnerId}/edit`;
Vue.use(VueApollo);
+Vue.use(VueRouter);
describe('GroupRunnerShowApp', () => {
let wrapper;
@@ -41,6 +46,8 @@ describe('GroupRunnerShowApp', () => {
const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton);
const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
+ const findRunnerDetailsTabs = () => wrapper.findComponent(RunnerDetailsTabs);
+ const findRunnersJobs = () => wrapper.findComponent(RunnersJobs);
const mockRunnerQueryResult = (runner = {}) => {
mockRunnerQuery = jest.fn().mockResolvedValue({
@@ -81,16 +88,23 @@ describe('GroupRunnerShowApp', () => {
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId });
});
- it('displays the header', async () => {
+ it('displays the runner header', () => {
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
});
- it('displays edit, pause, delete buttons', async () => {
- expect(findRunnerEditButton().exists()).toBe(true);
+ it('displays the runner edit and pause buttons', async () => {
+ expect(findRunnerEditButton().attributes('href')).toBe(mockEditGroupRunnerPath);
expect(findRunnerPauseButton().exists()).toBe(true);
expect(findRunnerDeleteButton().exists()).toBe(true);
});
+ it('shows runner details', () => {
+ expect(findRunnerDetailsTabs().props()).toEqual({
+ runner: mockRunner,
+ showAccessHelp: true,
+ });
+ });
+
it('shows basic runner details', () => {
const expected = `Description My Runner
Last contact Never contacted
@@ -104,17 +118,12 @@ describe('GroupRunnerShowApp', () => {
Token expiry
Runner authentication token expiration
Runner authentication tokens will expire based on a set interval.
- They will automatically rotate once expired. Learn more
- Never expires
+ They will automatically rotate once expired. Learn more Never expires
Tags None`.replace(/\s+/g, ' ');
expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
});
- it('renders runner details component', () => {
- expect(findRunnerDetails().props('runner')).toEqual(mockRunner);
- });
-
describe('when runner cannot be updated', () => {
beforeEach(async () => {
mockRunnerQueryResult({
@@ -129,7 +138,7 @@ describe('GroupRunnerShowApp', () => {
});
});
- it('does not display edit and pause buttons', () => {
+ it('does not display the runner edit and pause buttons', () => {
expect(findRunnerEditButton().exists()).toBe(false);
expect(findRunnerPauseButton().exists()).toBe(false);
});
@@ -153,7 +162,7 @@ describe('GroupRunnerShowApp', () => {
});
});
- it('does not display delete button', () => {
+ it('does not display the delete button', () => {
expect(findRunnerDeleteButton().exists()).toBe(false);
});
@@ -187,8 +196,17 @@ describe('GroupRunnerShowApp', () => {
mockRunnerQueryResult();
createComponent();
+
expect(findRunnerDetails().exists()).toBe(false);
});
+
+ it('does not show runner jobs', () => {
+ mockRunnerQueryResult();
+
+ createComponent();
+
+ expect(findRunnersJobs().exists()).toBe(false);
+ });
});
describe('When there is an error', () => {
diff --git a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
index 1e5bb828dbf..39ea5cade28 100644
--- a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
@@ -25,6 +25,7 @@ import RunnerActionsCell from '~/ci/runner/components/cells/runner_actions_cell.
import RegistrationDropdown from '~/ci/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/ci/runner/components/runner_pagination.vue';
import RunnerMembershipToggle from '~/ci/runner/components/runner_membership_toggle.vue';
+import RunnerJobStatusBadge from '~/ci/runner/components/runner_job_status_badge.vue';
import {
CREATED_ASC,
@@ -35,6 +36,7 @@ import {
I18N_STATUS_STALE,
INSTANCE_TYPE,
GROUP_TYPE,
+ JOBS_ROUTE_PATH,
PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
@@ -112,7 +114,6 @@ describe('GroupRunnersApp', () => {
propsData: {
registrationToken: mockRegistrationToken,
groupFullPath: mockGroupFullPath,
- groupRunnersLimitedCount: mockGroupRunnersCount,
...props,
},
provide: {
@@ -254,7 +255,7 @@ describe('GroupRunnersApp', () => {
let showToast;
const { webUrl, editUrl, node } = mockGroupRunnersEdges[0];
- const { id: graphqlId, shortSha } = node;
+ const { id: graphqlId, shortSha, jobExecutionStatus } = node;
const id = getIdFromGraphQLId(graphqlId);
const COUNT_QUERIES = 6; // Smart queries that display a filtered count of runners
const FILTERED_COUNT_QUERIES = 6; // Smart queries that display a count of runners in tabs and single stats
@@ -264,6 +265,13 @@ describe('GroupRunnersApp', () => {
showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show');
});
+ it('Shows job status and links to jobs', () => {
+ const badge = findRunnerRow(id).findByTestId('td-status').findComponent(RunnerJobStatusBadge);
+
+ expect(badge.props('jobStatus')).toBe(jobExecutionStatus);
+ expect(badge.attributes('href')).toBe(`${webUrl}#${JOBS_ROUTE_PATH}`);
+ });
+
it('view link is displayed correctly', () => {
const viewLink = findRunnerRow(id).findByTestId('td-summary').findComponent(GlLink);
@@ -466,7 +474,6 @@ describe('GroupRunnersApp', () => {
propsData: {
registrationToken: mockRegistrationToken,
groupFullPath: mockGroupFullPath,
- groupRunnersLimitedCount: mockGroupRunnersCount,
},
});
});
@@ -482,7 +489,6 @@ describe('GroupRunnersApp', () => {
propsData: {
registrationToken: null,
groupFullPath: mockGroupFullPath,
- groupRunnersLimitedCount: mockGroupRunnersCount,
},
});
});
diff --git a/spec/frontend/ci/runner/mock_data.js b/spec/frontend/ci/runner/mock_data.js
index 525756ed513..5cdf0ea4e3b 100644
--- a/spec/frontend/ci/runner/mock_data.js
+++ b/spec/frontend/ci/runner/mock_data.js
@@ -304,6 +304,7 @@ export const mockSearchExamples = [
export const onlineContactTimeoutSecs = 2 * 60 * 60;
export const staleTimeoutSecs = 7889238; // Ruby's `3.months`
+export const newRunnerPath = '/runners/new';
export const emptyStateSvgPath = 'emptyStateSvgPath.svg';
export const emptyStateFilteredSvgPath = 'emptyStateFilteredSvgPath.svg';