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:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-05-19 10:33:21 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-05-19 10:33:21 +0300
commit36a59d088eca61b834191dacea009677a96c052f (patch)
treee4f33972dab5d8ef79e3944a9f403035fceea43f /spec/frontend/pipelines
parenta1761f15ec2cae7c7f7bbda39a75494add0dfd6f (diff)
Add latest changes from gitlab-org/gitlab@15-0-stable-eev15.0.0-rc42
Diffstat (limited to 'spec/frontend/pipelines')
-rw-r--r--spec/frontend/pipelines/__snapshots__/utils_spec.js.snap11
-rw-r--r--spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js87
-rw-r--r--spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js117
-rw-r--r--spec/frontend/pipelines/components/jobs/utils_spec.js14
-rw-r--r--spec/frontend/pipelines/components/pipeline_tabs_spec.js9
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js (renamed from spec/frontend/pipelines/empty_state/ci_templates_spec.js)47
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js138
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js (renamed from spec/frontend/pipelines/empty_state/pipelines_ci_templates_spec.js)5
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js6
-rw-r--r--spec/frontend/pipelines/empty_state_spec.js60
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js27
-rw-r--r--spec/frontend/pipelines/graph/job_item_spec.js184
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js336
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_column_spec.js1
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_mock_data.js5
-rw-r--r--spec/frontend/pipelines/graph/mock_data.js122
-rw-r--r--spec/frontend/pipelines/graph_shared/links_inner_spec.js15
-rw-r--r--spec/frontend/pipelines/header_component_spec.js51
-rw-r--r--spec/frontend/pipelines/mock_data.js215
-rw-r--r--spec/frontend/pipelines/pipeline_graph/utils_spec.js20
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js2
-rw-r--r--spec/frontend/pipelines/test_reports/stores/actions_spec.js7
-rw-r--r--spec/frontend/pipelines/test_reports/stores/mutations_spec.js21
-rw-r--r--spec/frontend/pipelines/test_reports/test_suite_table_spec.js57
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js1
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_source_token_spec.js1
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_status_token_spec.js1
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js1
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js1
29 files changed, 1327 insertions, 235 deletions
diff --git a/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap b/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
index 2d2e5db598a..724ec7366d3 100644
--- a/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
+++ b/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
@@ -11,6 +11,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "6",
+ "kind": "BUILD",
"name": "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
@@ -53,6 +54,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "11",
+ "kind": "BUILD",
"name": "build_b",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
@@ -95,6 +97,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "16",
+ "kind": "BUILD",
"name": "build_c",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
@@ -137,6 +140,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "21",
+ "kind": "BUILD",
"name": "build_d 1/3",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
@@ -163,6 +167,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "24",
+ "kind": "BUILD",
"name": "build_d 2/3",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
@@ -189,6 +194,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "27",
+ "kind": "BUILD",
"name": "build_d 3/3",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
@@ -231,6 +237,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "59",
+ "kind": "BUILD",
"name": "test_c",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
@@ -275,6 +282,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "34",
+ "kind": "BUILD",
"name": "test_a",
"needs": Array [
"build_c",
@@ -325,6 +333,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "42",
+ "kind": "BUILD",
"name": "test_b 1/2",
"needs": Array [
"build_d 3/3",
@@ -363,6 +372,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "67",
+ "kind": "BUILD",
"name": "test_b 2/2",
"needs": Array [
"build_d 3/3",
@@ -417,6 +427,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "53",
+ "kind": "BUILD",
"name": "test_d",
"needs": Array [
"build_b",
diff --git a/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js b/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js
new file mode 100644
index 00000000000..3b5632a8a4e
--- /dev/null
+++ b/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js
@@ -0,0 +1,87 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+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 createFlash from '~/flash';
+import FailedJobsApp from '~/pipelines/components/jobs/failed_jobs_app.vue';
+import FailedJobsTable from '~/pipelines/components/jobs/failed_jobs_table.vue';
+import GetFailedJobsQuery from '~/pipelines/graphql/queries/get_failed_jobs.query.graphql';
+import { mockFailedJobsQueryResponse, mockFailedJobsSummaryData } from '../../mock_data';
+
+Vue.use(VueApollo);
+
+jest.mock('~/flash');
+
+describe('Failed Jobs App', () => {
+ let wrapper;
+ let resolverSpy;
+
+ const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
+ const findJobsTable = () => wrapper.findComponent(FailedJobsTable);
+
+ const createMockApolloProvider = (resolver) => {
+ const requestHandlers = [[GetFailedJobsQuery, resolver]];
+
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = (resolver, failedJobsSummaryData = mockFailedJobsSummaryData) => {
+ wrapper = shallowMount(FailedJobsApp, {
+ provide: {
+ fullPath: 'root/ci-project',
+ pipelineIid: 1,
+ },
+ propsData: {
+ failedJobsSummary: failedJobsSummaryData,
+ },
+ apolloProvider: createMockApolloProvider(resolver),
+ });
+ };
+
+ beforeEach(() => {
+ resolverSpy = jest.fn().mockResolvedValue(mockFailedJobsQueryResponse);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('loading spinner', () => {
+ beforeEach(() => {
+ createComponent(resolverSpy);
+ });
+
+ it('displays loading spinner when fetching failed jobs', () => {
+ expect(findLoadingSpinner().exists()).toBe(true);
+ });
+
+ it('hides loading spinner after the failed jobs have been fetched', async () => {
+ await waitForPromises();
+
+ expect(findLoadingSpinner().exists()).toBe(false);
+ });
+ });
+
+ it('displays the failed jobs table', async () => {
+ createComponent(resolverSpy);
+
+ await waitForPromises();
+
+ expect(findJobsTable().exists()).toBe(true);
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('handles query fetch error correctly', async () => {
+ resolverSpy = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+
+ createComponent(resolverSpy);
+
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was a problem fetching the failed jobs.',
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js b/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js
new file mode 100644
index 00000000000..b597a3bf4b0
--- /dev/null
+++ b/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js
@@ -0,0 +1,117 @@
+import { GlButton, GlLink, GlTableLite } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createFlash from '~/flash';
+import { redirectTo } from '~/lib/utils/url_utility';
+import FailedJobsTable from '~/pipelines/components/jobs/failed_jobs_table.vue';
+import RetryFailedJobMutation from '~/pipelines/graphql/mutations/retry_failed_job.mutation.graphql';
+import {
+ successRetryMutationResponse,
+ failedRetryMutationResponse,
+ mockPreparedFailedJobsData,
+ mockPreparedFailedJobsDataNoPermission,
+} from '../../mock_data';
+
+jest.mock('~/flash');
+jest.mock('~/lib/utils/url_utility');
+
+Vue.use(VueApollo);
+
+describe('Failed Jobs Table', () => {
+ let wrapper;
+
+ const successRetryMutationHandler = jest.fn().mockResolvedValue(successRetryMutationResponse);
+ const failedRetryMutationHandler = jest.fn().mockResolvedValue(failedRetryMutationResponse);
+
+ const findJobsTable = () => wrapper.findComponent(GlTableLite);
+ const findRetryButton = () => wrapper.findComponent(GlButton);
+ const findJobLink = () => wrapper.findComponent(GlLink);
+ const findJobLog = () => wrapper.findByTestId('job-log');
+
+ const createMockApolloProvider = (resolver) => {
+ const requestHandlers = [[RetryFailedJobMutation, resolver]];
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = (resolver, failedJobsData = mockPreparedFailedJobsData) => {
+ wrapper = mountExtended(FailedJobsTable, {
+ propsData: {
+ failedJobs: failedJobsData,
+ },
+ apolloProvider: createMockApolloProvider(resolver),
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays the failed jobs table', () => {
+ createComponent();
+
+ expect(findJobsTable().exists()).toBe(true);
+ });
+
+ it('calls the retry failed job mutation correctly', () => {
+ createComponent(successRetryMutationHandler);
+
+ findRetryButton().trigger('click');
+
+ expect(successRetryMutationHandler).toHaveBeenCalledWith({
+ id: mockPreparedFailedJobsData[0].id,
+ });
+ });
+
+ it('redirects to the new job after the mutation', async () => {
+ const {
+ data: {
+ jobRetry: { job },
+ },
+ } = successRetryMutationResponse;
+
+ createComponent(successRetryMutationHandler);
+
+ findRetryButton().trigger('click');
+
+ await waitForPromises();
+
+ expect(redirectTo).toHaveBeenCalledWith(job.detailedStatus.detailsPath);
+ });
+
+ it('shows error message if the retry failed job mutation fails', async () => {
+ createComponent(failedRetryMutationHandler);
+
+ findRetryButton().trigger('click');
+
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was a problem retrying the failed job.',
+ });
+ });
+
+ it('hides the job log and retry button if a user does not have permission', () => {
+ createComponent([[]], mockPreparedFailedJobsDataNoPermission);
+
+ expect(findJobLog().exists()).toBe(false);
+ expect(findRetryButton().exists()).toBe(false);
+ });
+
+ it('displays the job log and retry button if a user has permission', () => {
+ createComponent();
+
+ expect(findJobLog().exists()).toBe(true);
+ expect(findRetryButton().exists()).toBe(true);
+ });
+
+ it('job name links to the correct job', () => {
+ createComponent();
+
+ expect(findJobLink().attributes('href')).toBe(
+ mockPreparedFailedJobsData[0].detailedStatus.detailsPath,
+ );
+ });
+});
diff --git a/spec/frontend/pipelines/components/jobs/utils_spec.js b/spec/frontend/pipelines/components/jobs/utils_spec.js
new file mode 100644
index 00000000000..720446cfda3
--- /dev/null
+++ b/spec/frontend/pipelines/components/jobs/utils_spec.js
@@ -0,0 +1,14 @@
+import { prepareFailedJobs } from '~/pipelines/components/jobs/utils';
+import {
+ mockFailedJobsData,
+ mockFailedJobsSummaryData,
+ mockPreparedFailedJobsData,
+} from '../../mock_data';
+
+describe('Utils', () => {
+ it('prepares failed jobs data correctly', () => {
+ expect(prepareFailedJobs(mockFailedJobsData, mockFailedJobsSummaryData)).toEqual(
+ mockPreparedFailedJobsData,
+ );
+ });
+});
diff --git a/spec/frontend/pipelines/components/pipeline_tabs_spec.js b/spec/frontend/pipelines/components/pipeline_tabs_spec.js
index e18c3edbad9..89002ee47a8 100644
--- a/spec/frontend/pipelines/components/pipeline_tabs_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_tabs_spec.js
@@ -21,14 +21,19 @@ describe('The Pipeline Tabs', () => {
const findPipelineApp = () => wrapper.findComponent(PipelineGraphWrapper);
const findTestsApp = () => wrapper.findComponent(TestReports);
+ const defaultProvide = {
+ defaultTabValue: '',
+ };
+
const createComponent = (propsData = {}) => {
wrapper = extendedWrapper(
shallowMount(PipelineTabs, {
propsData,
+ provide: {
+ ...defaultProvide,
+ },
stubs: {
- Dag: { template: '<div id="dag"/>' },
JobsApp: { template: '<div class="jobs" />' },
- PipelineGraph: { template: '<div id="graph" />' },
TestReports: { template: '<div id="tests" />' },
},
}),
diff --git a/spec/frontend/pipelines/empty_state/ci_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js
index 606fdc9cac1..6531a15ab8e 100644
--- a/spec/frontend/pipelines/empty_state/ci_templates_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js
@@ -13,34 +13,35 @@ describe('CI Templates', () => {
let wrapper;
let trackingSpy;
- const createWrapper = () => {
- return shallowMountExtended(CiTemplates, {
+ const createWrapper = (propsData = {}) => {
+ wrapper = shallowMountExtended(CiTemplates, {
provide: {
pipelineEditorPath,
suggestedCiTemplates,
},
+ propsData,
});
};
const findTemplateDescription = () => wrapper.findByTestId('template-description');
const findTemplateLink = () => wrapper.findByTestId('template-link');
+ const findTemplateNames = () => wrapper.findAllByTestId('template-name');
const findTemplateName = () => wrapper.findByTestId('template-name');
const findTemplateLogo = () => wrapper.findByTestId('template-logo');
- beforeEach(() => {
- wrapper = createWrapper();
- });
-
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('renders template list', () => {
- it('renders all suggested templates', () => {
- const content = wrapper.text();
+ beforeEach(() => {
+ createWrapper();
+ });
- expect(content).toContain('Android', 'Bash', 'C++');
+ it('renders all suggested templates', () => {
+ expect(findTemplateNames().length).toBe(3);
+ expect(wrapper.text()).toContain('Android', 'Bash', 'C++');
});
it('has the correct template name', () => {
@@ -53,9 +54,13 @@ describe('CI Templates', () => {
);
});
+ it('has the link button enabled', () => {
+ expect(findTemplateLink().props('disabled')).toBe(false);
+ });
+
it('has the description of the template', () => {
expect(findTemplateDescription().text()).toBe(
- 'CI/CD template to test and deploy your Android project.',
+ 'Continuous integration and deployment template to test and deploy your Android project.',
);
});
@@ -64,8 +69,30 @@ describe('CI Templates', () => {
});
});
+ describe('filtering the templates', () => {
+ beforeEach(() => {
+ createWrapper({ filterTemplates: ['Bash'] });
+ });
+
+ it('renders only the filtered templates', () => {
+ expect(findTemplateNames()).toHaveLength(1);
+ expect(findTemplateName().text()).toBe('Bash');
+ });
+ });
+
+ describe('disabling the templates', () => {
+ beforeEach(() => {
+ createWrapper({ disabled: true });
+ });
+
+ it('has the link button disabled', () => {
+ expect(findTemplateLink().props('disabled')).toBe(true);
+ });
+ });
+
describe('tracking', () => {
beforeEach(() => {
+ createWrapper();
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js
new file mode 100644
index 00000000000..0c2938921d6
--- /dev/null
+++ b/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js
@@ -0,0 +1,138 @@
+import '~/commons';
+import { nextTick } from 'vue';
+import { GlPopover, GlButton } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+import IosTemplates from '~/pipelines/components/pipelines_list/empty_state/ios_templates.vue';
+import CiTemplates from '~/pipelines/components/pipelines_list/empty_state/ci_templates.vue';
+
+const pipelineEditorPath = '/-/ci/editor';
+const registrationToken = 'SECRET_TOKEN';
+const iOSTemplateName = 'iOS-Fastlane';
+
+describe('iOS Templates', () => {
+ let wrapper;
+
+ const createWrapper = (providedPropsData = {}) => {
+ return shallowMountExtended(IosTemplates, {
+ provide: {
+ pipelineEditorPath,
+ iosRunnersAvailable: true,
+ ...providedPropsData,
+ },
+ propsData: {
+ registrationToken,
+ },
+ stubs: {
+ GlButton,
+ },
+ });
+ };
+
+ const findIosTemplate = () => wrapper.findComponent(CiTemplates);
+ const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
+ const findRunnerInstructionsPopover = () => wrapper.findComponent(GlPopover);
+ const findRunnerSetupTodoEmoji = () => wrapper.findByTestId('runner-setup-marked-todo');
+ const findRunnerSetupCompletedEmoji = () => wrapper.findByTestId('runner-setup-marked-completed');
+ const findSetupRunnerLink = () => wrapper.findByText('Set up a runner');
+ const configurePipelineLink = () => wrapper.findByTestId('configure-pipeline-link');
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when ios runners are not available', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ iosRunnersAvailable: false });
+ });
+
+ describe('the runner setup section', () => {
+ it('marks the section as todo', () => {
+ expect(findRunnerSetupTodoEmoji().isVisible()).toBe(true);
+ expect(findRunnerSetupCompletedEmoji().isVisible()).toBe(false);
+ });
+
+ it('renders the setup runner link', () => {
+ expect(findSetupRunnerLink().exists()).toBe(true);
+ });
+
+ it('renders the runner instructions modal with a popover once clicked', async () => {
+ findSetupRunnerLink().element.parentElement.click();
+
+ await nextTick();
+
+ expect(findRunnerInstructionsModal().exists()).toBe(true);
+ expect(findRunnerInstructionsModal().props('registrationToken')).toBe(registrationToken);
+ expect(findRunnerInstructionsModal().props('defaultPlatformName')).toBe('osx');
+
+ findRunnerInstructionsModal().vm.$emit('shown');
+
+ await nextTick();
+
+ expect(findRunnerInstructionsPopover().exists()).toBe(true);
+ });
+ });
+
+ describe('the configure pipeline section', () => {
+ it('has a disabled link button', () => {
+ expect(configurePipelineLink().props('disabled')).toBe(true);
+ });
+ });
+
+ describe('the ios-Fastlane template', () => {
+ it('renders the template', () => {
+ expect(findIosTemplate().props('filterTemplates')).toStrictEqual([iOSTemplateName]);
+ });
+
+ it('has a disabled link button', () => {
+ expect(findIosTemplate().props('disabled')).toBe(true);
+ });
+ });
+ });
+
+ describe('when ios runners are available', () => {
+ beforeEach(() => {
+ wrapper = createWrapper();
+ });
+
+ describe('the runner setup section', () => {
+ it('marks the section as completed', () => {
+ expect(findRunnerSetupTodoEmoji().isVisible()).toBe(false);
+ expect(findRunnerSetupCompletedEmoji().isVisible()).toBe(true);
+ });
+
+ it('does not render the setup runner link', () => {
+ expect(findSetupRunnerLink().exists()).toBe(false);
+ });
+ });
+
+ describe('the configure pipeline section', () => {
+ it('has an enabled link button', () => {
+ expect(configurePipelineLink().props('disabled')).toBe(false);
+ });
+
+ it('links to the pipeline editor with the right template', () => {
+ expect(configurePipelineLink().attributes('href')).toBe(
+ `${pipelineEditorPath}?template=${iOSTemplateName}`,
+ );
+ });
+ });
+
+ describe('the ios-Fastlane template', () => {
+ it('renders the template', () => {
+ expect(findIosTemplate().props('filterTemplates')).toStrictEqual([iOSTemplateName]);
+ });
+
+ it('has an enabled link button', () => {
+ expect(findIosTemplate().props('disabled')).toBe(false);
+ });
+
+ it('links to the pipeline editor with the right template', () => {
+ expect(configurePipelineLink().attributes('href')).toBe(
+ `${pipelineEditorPath}?template=${iOSTemplateName}`,
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/empty_state/pipelines_ci_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
index 14860f20317..b537c81da3f 100644
--- a/spec/frontend/pipelines/empty_state/pipelines_ci_templates_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
@@ -16,6 +16,7 @@ import {
} from '~/pipeline_editor/constants';
const pipelineEditorPath = '/-/ci/editor';
+const ciRunnerSettingsPath = '/-/settings/ci_cd';
jest.mock('~/experimentation/experiment_tracking');
@@ -27,8 +28,10 @@ describe('Pipelines CI Templates', () => {
return shallowMountExtended(PipelinesCiTemplates, {
provide: {
pipelineEditorPath,
+ ciRunnerSettingsPath,
+ anyRunnersAvailable: true,
+ ...propsData,
},
- propsData,
stubs,
});
};
diff --git a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js
index 93bc8faa51b..6d0e99ff63e 100644
--- a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js
@@ -1,6 +1,7 @@
import { GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import axios from '~/lib/utils/axios_utils';
import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue';
import eventHub from '~/pipelines/event_hub';
@@ -48,11 +49,12 @@ describe('Pipelines stage component', () => {
mock.restore();
});
+ const findCiActionBtn = () => wrapper.find('.js-ci-action');
+ const findCiIcon = () => wrapper.findComponent(CiIcon);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownToggle = () => wrapper.find('button.dropdown-toggle');
const findDropdownMenu = () =>
wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]');
- const findCiActionBtn = () => wrapper.find('.js-ci-action');
const findMergeTrainWarning = () => wrapper.find('[data-testid="warning-message-merge-trains"]');
const openStageDropdown = () => {
@@ -74,7 +76,7 @@ describe('Pipelines stage component', () => {
it('should render a dropdown with the status icon', () => {
expect(findDropdown().exists()).toBe(true);
expect(findDropdownToggle().exists()).toBe(true);
- expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true);
+ expect(findCiIcon().exists()).toBe(true);
});
});
diff --git a/spec/frontend/pipelines/empty_state_spec.js b/spec/frontend/pipelines/empty_state_spec.js
index 46dad4a035c..0abf7f59717 100644
--- a/spec/frontend/pipelines/empty_state_spec.js
+++ b/spec/frontend/pipelines/empty_state_spec.js
@@ -1,7 +1,11 @@
import '~/commons';
-import { mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import { GlEmptyState } from '@gitlab/ui';
+import { stubExperiments } from 'helpers/experimentation_helper';
import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue';
+import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue';
+import IosTemplates from '~/pipelines/components/pipelines_list/empty_state/ios_templates.vue';
describe('Pipelines Empty State', () => {
let wrapper;
@@ -9,44 +13,68 @@ describe('Pipelines Empty State', () => {
const findIllustration = () => wrapper.find('img');
const findButton = () => wrapper.find('a');
const pipelinesCiTemplates = () => wrapper.findComponent(PipelinesCiTemplates);
+ const iosTemplates = () => wrapper.findComponent(IosTemplates);
const createWrapper = (props = {}) => {
- wrapper = mount(EmptyState, {
+ wrapper = shallowMount(EmptyState, {
provide: {
pipelineEditorPath: '',
suggestedCiTemplates: [],
+ anyRunnersAvailable: true,
+ ciRunnerSettingsPath: '',
},
propsData: {
emptyStateSvgPath: 'foo.svg',
canSetCi: true,
...props,
},
+ stubs: {
+ GlEmptyState,
+ GitlabExperiment,
+ },
});
};
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
describe('when user can configure CI', () => {
- beforeEach(() => {
- createWrapper({}, mount);
- });
+ describe('when the ios_specific_templates experiment is active', () => {
+ beforeEach(() => {
+ stubExperiments({ ios_specific_templates: 'candidate' });
+ createWrapper();
+ });
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
+ it('should render the iOS templates', () => {
+ expect(iosTemplates().exists()).toBe(true);
+ });
+
+ it('should not render the CI/CD templates', () => {
+ expect(pipelinesCiTemplates().exists()).toBe(false);
+ });
});
- it('should render the CI/CD templates', () => {
- expect(pipelinesCiTemplates().exists()).toBe(true);
+ describe('when the ios_specific_templates experiment is inactive', () => {
+ beforeEach(() => {
+ stubExperiments({ ios_specific_templates: 'control' });
+ createWrapper();
+ });
+
+ it('should render the CI/CD templates', () => {
+ expect(pipelinesCiTemplates().exists()).toBe(true);
+ });
+
+ it('should not render the iOS templates', () => {
+ expect(iosTemplates().exists()).toBe(false);
+ });
});
});
describe('when user cannot configure CI', () => {
beforeEach(() => {
- createWrapper({ canSetCi: false }, mount);
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
+ createWrapper({ canSetCi: false });
});
it('should render empty state SVG', () => {
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index cb7073fb5f5..49d64c6eac0 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -16,7 +16,7 @@ import {
} from '~/performance/constants';
import * as perfUtils from '~/performance/utils';
import {
- IID_FAILURE,
+ ACTION_FAILURE,
LAYER_VIEW,
STAGE_VIEW,
VIEW_TYPE_KEY,
@@ -188,7 +188,9 @@ describe('Pipeline graph wrapper', () => {
it('displays the no iid alert', () => {
expect(getAlert().exists()).toBe(true);
- expect(getAlert().text()).toBe(wrapper.vm.$options.errorTexts[IID_FAILURE]);
+ expect(getAlert().text()).toBe(
+ 'The data in this pipeline is too old to be rendered as a graph. Please check the Jobs tab to access historical data.',
+ );
});
it('does not display the graph', () => {
@@ -196,6 +198,27 @@ describe('Pipeline graph wrapper', () => {
});
});
+ describe('when there is an error with an action in the graph', () => {
+ beforeEach(async () => {
+ createComponentWithApollo();
+ await waitForPromises();
+ await getGraph().vm.$emit('error', { type: ACTION_FAILURE });
+ });
+
+ it('does not display the loading icon', () => {
+ expect(getLoadingIcon().exists()).toBe(false);
+ });
+
+ it('displays the action error alert', () => {
+ expect(getAlert().exists()).toBe(true);
+ expect(getAlert().text()).toBe('An error occurred while performing this action.');
+ });
+
+ it('displays the graph', () => {
+ expect(getGraph().exists()).toBe(true);
+ });
+ });
+
describe('when refresh action is emitted', () => {
beforeEach(async () => {
createComponentWithApollo();
diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js
index 23e7ed7ebb4..4f0da09fec6 100644
--- a/spec/frontend/pipelines/graph/job_item_spec.js
+++ b/spec/frontend/pipelines/graph/job_item_spec.js
@@ -1,89 +1,34 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { GlBadge } from '@gitlab/ui';
import JobItem from '~/pipelines/components/graph/job_item.vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import {
+ delayedJob,
+ mockJob,
+ mockJobWithoutDetails,
+ mockJobWithUnauthorizedAction,
+ triggerJob,
+} from './mock_data';
describe('pipeline graph job item', () => {
let wrapper;
- const findJobWithoutLink = () => wrapper.find('[data-testid="job-without-link"]');
- const findJobWithLink = () => wrapper.find('[data-testid="job-with-link"]');
- const findActionComponent = () => wrapper.find('[data-testid="ci-action-component"]');
+ const findJobWithoutLink = () => wrapper.findByTestId('job-without-link');
+ const findJobWithLink = () => wrapper.findByTestId('job-with-link');
+ const findActionComponent = () => wrapper.findByTestId('ci-action-component');
+ const findBadge = () => wrapper.findComponent(GlBadge);
const createWrapper = (propsData) => {
- wrapper = mount(JobItem, {
- propsData,
- });
+ wrapper = extendedWrapper(
+ mount(JobItem, {
+ propsData,
+ }),
+ );
};
const triggerActiveClass = 'gl-shadow-x0-y0-b3-s1-blue-500';
- const delayedJob = {
- __typename: 'CiJob',
- name: 'delayed job',
- scheduledAt: '2015-07-03T10:01:00.000Z',
- needs: [],
- status: {
- __typename: 'DetailedStatus',
- icon: 'status_scheduled',
- tooltip: 'delayed manual action (%{remainingTime})',
- hasDetails: true,
- detailsPath: '/root/kinder-pipe/-/jobs/5339',
- group: 'scheduled',
- action: {
- __typename: 'StatusAction',
- icon: 'time-out',
- title: 'Unschedule',
- path: '/frontend-fixtures/builds-project/-/jobs/142/unschedule',
- buttonTitle: 'Unschedule job',
- },
- },
- };
-
- const mockJob = {
- id: 4256,
- name: 'test',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- tooltip: 'passed',
- group: 'success',
- detailsPath: '/root/ci-mock/builds/4256',
- hasDetails: true,
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/root/ci-mock/builds/4256/retry',
- method: 'post',
- },
- },
- };
- const mockJobWithoutDetails = {
- id: 4257,
- name: 'job_without_details',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- detailsPath: '/root/ci-mock/builds/4257',
- hasDetails: false,
- },
- };
- const mockJobWithUnauthorizedAction = {
- id: 4258,
- name: 'stop-environment',
- status: {
- icon: 'status_manual',
- label: 'manual stop action (not allowed)',
- tooltip: 'manual action',
- group: 'manual',
- detailsPath: '/root/ci-mock/builds/4258',
- hasDetails: true,
- action: null,
- },
- };
-
afterEach(() => {
wrapper.destroy();
});
@@ -148,13 +93,25 @@ describe('pipeline graph job item', () => {
});
});
- it('should render provided class name', () => {
- createWrapper({
- job: mockJob,
- cssClassJobName: 'css-class-job-name',
+ describe('job style', () => {
+ beforeEach(() => {
+ createWrapper({
+ job: mockJob,
+ cssClassJobName: 'css-class-job-name',
+ });
+ });
+
+ it('should render provided class name', () => {
+ expect(wrapper.find('a').classes()).toContain('css-class-job-name');
+ });
+
+ it('does not show a badge on the job item', () => {
+ expect(findBadge().exists()).toBe(false);
});
- expect(wrapper.find('a').classes()).toContain('css-class-job-name');
+ it('does not apply the trigger job class', () => {
+ expect(findJobWithLink().classes()).not.toContain('gl-rounded-lg');
+ });
});
describe('status label', () => {
@@ -201,34 +158,51 @@ describe('pipeline graph job item', () => {
});
});
- describe('trigger job highlighting', () => {
- it.each`
- job | jobName | expanded | link
- ${mockJob} | ${mockJob.name} | ${true} | ${true}
- ${mockJobWithoutDetails} | ${mockJobWithoutDetails.name} | ${true} | ${false}
- `(
- `trigger job should stay highlighted when downstream is expanded`,
- ({ job, jobName, expanded, link }) => {
- createWrapper({ job, pipelineExpanded: { jobName, expanded } });
- const findJobEl = link ? findJobWithLink : findJobWithoutLink;
-
- expect(findJobEl().classes()).toContain(triggerActiveClass);
- },
- );
+ describe('trigger job', () => {
+ describe('card', () => {
+ beforeEach(() => {
+ createWrapper({ job: triggerJob });
+ });
- it.each`
- job | jobName | expanded | link
- ${mockJob} | ${mockJob.name} | ${false} | ${true}
- ${mockJobWithoutDetails} | ${mockJobWithoutDetails.name} | ${false} | ${false}
- `(
- `trigger job should not be highlighted when downstream is not expanded`,
- ({ job, jobName, expanded, link }) => {
- createWrapper({ job, pipelineExpanded: { jobName, expanded } });
- const findJobEl = link ? findJobWithLink : findJobWithoutLink;
-
- expect(findJobEl().classes()).not.toContain(triggerActiveClass);
- },
- );
+ it('shows a badge on the job item', () => {
+ expect(findBadge().exists()).toBe(true);
+ expect(findBadge().text()).toBe('Trigger job');
+ });
+
+ it('applies a rounded corner style instead of the usual pill shape', () => {
+ expect(findJobWithoutLink().classes()).toContain('gl-rounded-lg');
+ });
+ });
+
+ describe('highlighting', () => {
+ it.each`
+ job | jobName | expanded | link
+ ${mockJob} | ${mockJob.name} | ${true} | ${true}
+ ${mockJobWithoutDetails} | ${mockJobWithoutDetails.name} | ${true} | ${false}
+ `(
+ `trigger job should stay highlighted when downstream is expanded`,
+ ({ job, jobName, expanded, link }) => {
+ createWrapper({ job, pipelineExpanded: { jobName, expanded } });
+ const findJobEl = link ? findJobWithLink : findJobWithoutLink;
+
+ expect(findJobEl().classes()).toContain(triggerActiveClass);
+ },
+ );
+
+ it.each`
+ job | jobName | expanded | link
+ ${mockJob} | ${mockJob.name} | ${false} | ${true}
+ ${mockJobWithoutDetails} | ${mockJobWithoutDetails.name} | ${false} | ${false}
+ `(
+ `trigger job should not be highlighted when downstream is not expanded`,
+ ({ job, jobName, expanded, link }) => {
+ createWrapper({ job, pipelineExpanded: { jobName, expanded } });
+ const findJobEl = link ? findJobWithLink : findJobWithoutLink;
+
+ expect(findJobEl().classes()).not.toContain(triggerActiveClass);
+ },
+ );
+ });
});
describe('job classes', () => {
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index d800a8c341e..06fd970778c 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -1,11 +1,21 @@
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlButton, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
-import { UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
+import { ACTION_FAILURE, UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
+import { PIPELINE_GRAPHQL_TYPE } from '~/pipelines/constants';
+import CancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
+import RetryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import mockPipeline from './linked_pipelines_mock_data';
+Vue.use(VueApollo);
+
describe('Linked pipeline', () => {
let wrapper;
@@ -27,22 +37,30 @@ describe('Linked pipeline', () => {
};
const findButton = () => wrapper.find(GlButton);
- const findDownstreamPipelineTitle = () => wrapper.find('[data-testid="downstream-title"]');
- const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]');
+ const findCancelButton = () => wrapper.findByLabelText('Cancel downstream pipeline');
+ const findCardTooltip = () => wrapper.findComponent(GlTooltip);
+ const findDownstreamPipelineTitle = () => wrapper.findByTestId('downstream-title');
+ const findExpandButton = () => wrapper.findByTestId('expand-pipeline-button');
const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' });
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findPipelineLink = () => wrapper.find('[data-testid="pipelineLink"]');
- const findExpandButton = () => wrapper.find('[data-testid="expand-pipeline-button"]');
-
- const createWrapper = (propsData, data = []) => {
- wrapper = mount(LinkedPipelineComponent, {
- propsData,
- data() {
- return {
- ...data,
- };
- },
- });
+ const findPipelineLabel = () => wrapper.findByTestId('downstream-pipeline-label');
+ const findPipelineLink = () => wrapper.findByTestId('pipelineLink');
+ const findRetryButton = () => wrapper.findByLabelText('Retry downstream pipeline');
+
+ const createWrapper = ({ propsData, downstreamRetryAction = false }) => {
+ const mockApollo = createMockApollo();
+
+ wrapper = extendedWrapper(
+ mount(LinkedPipelineComponent, {
+ propsData,
+ provide: {
+ glFeatures: {
+ downstreamRetryAction,
+ },
+ },
+ apolloProvider: mockApollo,
+ }),
+ );
};
afterEach(() => {
@@ -59,7 +77,7 @@ describe('Linked pipeline', () => {
};
beforeEach(() => {
- createWrapper(props);
+ createWrapper({ propsData: props });
});
it('should render the project name', () => {
@@ -84,18 +102,13 @@ describe('Linked pipeline', () => {
expect(wrapper.text()).toContain(`#${props.pipeline.id}`);
});
- it('should correctly compute the tooltip text', () => {
- expect(wrapper.vm.tooltipText).toContain(mockPipeline.project.name);
- expect(wrapper.vm.tooltipText).toContain(mockPipeline.status.label);
- expect(wrapper.vm.tooltipText).toContain(mockPipeline.sourceJob.name);
- expect(wrapper.vm.tooltipText).toContain(mockPipeline.id);
- });
+ it('adds the card tooltip text to the DOM', () => {
+ expect(findCardTooltip().exists()).toBe(true);
- it('should render the tooltip text as the title attribute', () => {
- const titleAttr = findLinkedPipeline().attributes('title');
-
- expect(titleAttr).toContain(mockPipeline.project.name);
- expect(titleAttr).toContain(mockPipeline.status.label);
+ expect(findCardTooltip().text()).toContain(mockPipeline.project.name);
+ expect(findCardTooltip().text()).toContain(mockPipeline.status.label);
+ expect(findCardTooltip().text()).toContain(mockPipeline.sourceJob.name);
+ expect(findCardTooltip().text()).toContain(mockPipeline.id);
});
it('should display multi-project label when pipeline project id is not the same as triggered pipeline project id', () => {
@@ -105,7 +118,7 @@ describe('Linked pipeline', () => {
describe('upstream pipelines', () => {
beforeEach(() => {
- createWrapper(upstreamProps);
+ createWrapper({ propsData: upstreamProps });
});
it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => {
@@ -123,45 +136,246 @@ describe('Linked pipeline', () => {
});
describe('downstream pipelines', () => {
- beforeEach(() => {
- createWrapper(downstreamProps);
- });
-
- it('parent/child label container should exist', () => {
- expect(findPipelineLabel().exists()).toBe(true);
- });
-
- it('should display child label when pipeline project id is the same as triggered pipeline project id', () => {
- expect(findPipelineLabel().exists()).toBe(true);
- });
-
- it('should have the name of the trigger job on the card when it is a child pipeline', () => {
- expect(findDownstreamPipelineTitle().text()).toBe(mockPipeline.sourceJob.name);
- });
-
- it('downstream pipeline should contain the correct link', () => {
- expect(findPipelineLink().attributes('href')).toBe(downstreamProps.pipeline.path);
+ describe('styling', () => {
+ beforeEach(() => {
+ createWrapper({ propsData: downstreamProps });
+ });
+
+ it('parent/child label container should exist', () => {
+ expect(findPipelineLabel().exists()).toBe(true);
+ });
+
+ it('should display child label when pipeline project id is the same as triggered pipeline project id', () => {
+ expect(findPipelineLabel().exists()).toBe(true);
+ });
+
+ it('should have the name of the trigger job on the card when it is a child pipeline', () => {
+ expect(findDownstreamPipelineTitle().text()).toBe(mockPipeline.sourceJob.name);
+ });
+
+ it('downstream pipeline should contain the correct link', () => {
+ expect(findPipelineLink().attributes('href')).toBe(downstreamProps.pipeline.path);
+ });
+
+ it('applies the flex-row css class to the card', () => {
+ expect(findLinkedPipeline().classes()).toContain('gl-flex-direction-row');
+ expect(findLinkedPipeline().classes()).not.toContain('gl-flex-direction-row-reverse');
+ });
});
- it('applies the flex-row css class to the card', () => {
- expect(findLinkedPipeline().classes()).toContain('gl-flex-direction-row');
- expect(findLinkedPipeline().classes()).not.toContain('gl-flex-direction-row-reverse');
+ describe('action button', () => {
+ describe('with the `downstream_retry_action` flag on', () => {
+ describe('with permissions', () => {
+ describe('on an upstream', () => {
+ describe('when retryable', () => {
+ beforeEach(() => {
+ const retryablePipeline = {
+ ...upstreamProps,
+ pipeline: { ...mockPipeline, retryable: true },
+ };
+
+ createWrapper({ propsData: retryablePipeline, downstreamRetryAction: true });
+ });
+
+ it('does not show the retry or cancel button', () => {
+ expect(findCancelButton().exists()).toBe(false);
+ expect(findRetryButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('on a downstream', () => {
+ describe('when retryable', () => {
+ beforeEach(() => {
+ const retryablePipeline = {
+ ...downstreamProps,
+ pipeline: { ...mockPipeline, retryable: true },
+ };
+
+ createWrapper({ propsData: retryablePipeline, downstreamRetryAction: true });
+ });
+
+ it('shows only the retry button', () => {
+ expect(findCancelButton().exists()).toBe(false);
+ expect(findRetryButton().exists()).toBe(true);
+ });
+
+ it('hides the card tooltip when the action button tooltip is hovered', async () => {
+ expect(findCardTooltip().exists()).toBe(true);
+
+ await findRetryButton().trigger('mouseover');
+
+ expect(findCardTooltip().exists()).toBe(false);
+ });
+
+ describe('and the retry button is clicked', () => {
+ describe('on success', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
+ jest.spyOn(wrapper.vm, '$emit');
+ await findRetryButton().trigger('click');
+ });
+
+ it('calls the retry mutation ', () => {
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: RetryPipelineMutation,
+ variables: {
+ id: convertToGraphQLId(PIPELINE_GRAPHQL_TYPE, mockPipeline.id),
+ },
+ });
+ });
+
+ it('emits the refreshPipelineGraph event', () => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('refreshPipelineGraph');
+ });
+ });
+
+ describe('on failure', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({ errors: [] });
+ jest.spyOn(wrapper.vm, '$emit');
+ await findRetryButton().trigger('click');
+ });
+
+ it('emits an error event', () => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('error', {
+ type: ACTION_FAILURE,
+ });
+ });
+ });
+ });
+ });
+
+ describe('when cancelable', () => {
+ beforeEach(() => {
+ const cancelablePipeline = {
+ ...downstreamProps,
+ pipeline: { ...mockPipeline, cancelable: true },
+ };
+
+ createWrapper({ propsData: cancelablePipeline, downstreamRetryAction: true });
+ });
+
+ it('shows only the cancel button ', () => {
+ expect(findCancelButton().exists()).toBe(true);
+ expect(findRetryButton().exists()).toBe(false);
+ });
+
+ it('hides the card tooltip when the action button tooltip is hovered', async () => {
+ expect(findCardTooltip().exists()).toBe(true);
+
+ await findCancelButton().trigger('mouseover');
+
+ expect(findCardTooltip().exists()).toBe(false);
+ });
+
+ describe('and the cancel button is clicked', () => {
+ describe('on success', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
+ jest.spyOn(wrapper.vm, '$emit');
+ await findCancelButton().trigger('click');
+ });
+
+ it('calls the cancel mutation', () => {
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: CancelPipelineMutation,
+ variables: {
+ id: convertToGraphQLId(PIPELINE_GRAPHQL_TYPE, mockPipeline.id),
+ },
+ });
+ });
+ it('emits the refreshPipelineGraph event', () => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('refreshPipelineGraph');
+ });
+ });
+ describe('on failure', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({ errors: [] });
+ jest.spyOn(wrapper.vm, '$emit');
+ await findCancelButton().trigger('click');
+ });
+ it('emits an error event', () => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('error', {
+ type: ACTION_FAILURE,
+ });
+ });
+ });
+ });
+ });
+
+ describe('when both cancellable and retryable', () => {
+ beforeEach(() => {
+ const pipelineWithTwoActions = {
+ ...downstreamProps,
+ pipeline: { ...mockPipeline, cancelable: true, retryable: true },
+ };
+
+ createWrapper({ propsData: pipelineWithTwoActions, downstreamRetryAction: true });
+ });
+
+ it('only shows the cancel button', () => {
+ expect(findRetryButton().exists()).toBe(false);
+ expect(findCancelButton().exists()).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('without permissions', () => {
+ beforeEach(() => {
+ const pipelineWithTwoActions = {
+ ...downstreamProps,
+ pipeline: {
+ ...mockPipeline,
+ cancelable: true,
+ retryable: true,
+ userPermissions: { updatePipeline: false },
+ },
+ };
+
+ createWrapper({ propsData: pipelineWithTwoActions });
+ });
+
+ it('does not show any action button', () => {
+ expect(findRetryButton().exists()).toBe(false);
+ expect(findCancelButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('with the `downstream_retry_action` flag off', () => {
+ beforeEach(() => {
+ const pipelineWithTwoActions = {
+ ...downstreamProps,
+ pipeline: { ...mockPipeline, cancelable: true, retryable: true },
+ };
+
+ createWrapper({ propsData: pipelineWithTwoActions });
+ });
+ it('does not show any action button', () => {
+ expect(findRetryButton().exists()).toBe(false);
+ expect(findCancelButton().exists()).toBe(false);
+ });
+ });
});
});
describe('expand button', () => {
it.each`
- pipelineType | anglePosition | borderClass | expanded
- ${downstreamProps} | ${'angle-right'} | ${'gl-border-l-1!'} | ${false}
- ${downstreamProps} | ${'angle-left'} | ${'gl-border-l-1!'} | ${true}
- ${upstreamProps} | ${'angle-left'} | ${'gl-border-r-1!'} | ${false}
- ${upstreamProps} | ${'angle-right'} | ${'gl-border-r-1!'} | ${true}
+ pipelineType | anglePosition | buttonBorderClasses | expanded
+ ${downstreamProps} | ${'angle-right'} | ${'gl-border-l-0!'} | ${false}
+ ${downstreamProps} | ${'angle-left'} | ${'gl-border-l-0!'} | ${true}
+ ${upstreamProps} | ${'angle-left'} | ${'gl-border-r-0!'} | ${false}
+ ${upstreamProps} | ${'angle-right'} | ${'gl-border-r-0!'} | ${true}
`(
- '$pipelineType.columnTitle pipeline button icon should be $anglePosition with $borderClass if expanded state is $expanded',
- ({ pipelineType, anglePosition, borderClass, expanded }) => {
- createWrapper({ ...pipelineType, expanded });
+ '$pipelineType.columnTitle pipeline button icon should be $anglePosition with $buttonBorderClasses if expanded state is $expanded',
+ ({ pipelineType, anglePosition, buttonBorderClasses, expanded }) => {
+ createWrapper({ propsData: { ...pipelineType, expanded } });
expect(findExpandButton().props('icon')).toBe(anglePosition);
- expect(findExpandButton().classes()).toContain(borderClass);
+ expect(findExpandButton().classes()).toContain(buttonBorderClasses);
},
);
});
@@ -176,7 +390,7 @@ describe('Linked pipeline', () => {
};
beforeEach(() => {
- createWrapper(props);
+ createWrapper({ propsData: props });
});
it('loading icon is visible', () => {
@@ -194,7 +408,7 @@ describe('Linked pipeline', () => {
};
beforeEach(() => {
- createWrapper(props);
+ createWrapper({ propsData: props });
});
it('emits `pipelineClicked` event', () => {
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
index 1673065e09c..46000711110 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
@@ -67,7 +67,6 @@ describe('Linked Pipelines Column', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
describe('it renders correctly', () => {
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js
index 955b70cbd3b..f7f5738e46d 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js
@@ -2,6 +2,11 @@ export default {
__typename: 'Pipeline',
id: 195,
iid: '5',
+ retryable: false,
+ cancelable: false,
+ userPermissions: {
+ updatePipeline: true,
+ },
path: '/root/elemenohpee/-/pipelines/195',
status: {
__typename: 'DetailedStatus',
diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js
index 0cf7dc507f4..6124d67af09 100644
--- a/spec/frontend/pipelines/graph/mock_data.js
+++ b/spec/frontend/pipelines/graph/mock_data.js
@@ -1,4 +1,5 @@
import { unwrapPipelineData } from '~/pipelines/components/graph/utils';
+import { BUILD_KIND, BRIDGE_KIND } from '~/pipelines/components/graph/constants';
export const mockPipelineResponse = {
data: {
@@ -50,6 +51,7 @@ export const mockPipelineResponse = {
{
__typename: 'CiJob',
id: '6',
+ kind: BUILD_KIND,
name: 'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
scheduledAt: null,
status: {
@@ -101,6 +103,7 @@ export const mockPipelineResponse = {
__typename: 'CiJob',
id: '11',
name: 'build_b',
+ kind: BUILD_KIND,
scheduledAt: null,
status: {
__typename: 'DetailedStatus',
@@ -151,6 +154,7 @@ export const mockPipelineResponse = {
__typename: 'CiJob',
id: '16',
name: 'build_c',
+ kind: BUILD_KIND,
scheduledAt: null,
status: {
__typename: 'DetailedStatus',
@@ -200,6 +204,7 @@ export const mockPipelineResponse = {
{
__typename: 'CiJob',
id: '21',
+ kind: BUILD_KIND,
name: 'build_d 1/3',
scheduledAt: null,
status: {
@@ -232,6 +237,7 @@ export const mockPipelineResponse = {
{
__typename: 'CiJob',
id: '24',
+ kind: BUILD_KIND,
name: 'build_d 2/3',
scheduledAt: null,
status: {
@@ -264,6 +270,7 @@ export const mockPipelineResponse = {
{
__typename: 'CiJob',
id: '27',
+ kind: BUILD_KIND,
name: 'build_d 3/3',
scheduledAt: null,
status: {
@@ -329,6 +336,7 @@ export const mockPipelineResponse = {
{
__typename: 'CiJob',
id: '34',
+ kind: BUILD_KIND,
name: 'test_a',
scheduledAt: null,
status: {
@@ -413,6 +421,7 @@ export const mockPipelineResponse = {
{
__typename: 'CiJob',
id: '42',
+ kind: BUILD_KIND,
name: 'test_b 1/2',
scheduledAt: null,
status: {
@@ -499,6 +508,7 @@ export const mockPipelineResponse = {
{
__typename: 'CiJob',
id: '67',
+ kind: BUILD_KIND,
name: 'test_b 2/2',
scheduledAt: null,
status: {
@@ -603,6 +613,7 @@ export const mockPipelineResponse = {
{
__typename: 'CiJob',
id: '59',
+ kind: BUILD_KIND,
name: 'test_c',
scheduledAt: null,
status: {
@@ -646,6 +657,7 @@ export const mockPipelineResponse = {
{
__typename: 'CiJob',
id: '53',
+ kind: BUILD_KIND,
name: 'test_d',
scheduledAt: null,
status: {
@@ -699,6 +711,11 @@ export const downstream = {
id: 175,
iid: '31',
path: '/root/elemenohpee/-/pipelines/175',
+ retryable: true,
+ cancelable: false,
+ userPermissions: {
+ updatePipeline: true,
+ },
status: {
id: '70',
group: 'success',
@@ -724,6 +741,11 @@ export const downstream = {
id: 181,
iid: '27',
path: '/root/abcd-dag/-/pipelines/181',
+ retryable: true,
+ cancelable: false,
+ userPermissions: {
+ updatePipeline: true,
+ },
status: {
id: '72',
group: 'success',
@@ -752,6 +774,11 @@ export const upstream = {
id: 161,
iid: '24',
path: '/root/abcd-dag/-/pipelines/161',
+ retryable: true,
+ cancelable: false,
+ userPermissions: {
+ updatePipeline: true,
+ },
status: {
id: '74',
group: 'success',
@@ -786,6 +813,11 @@ export const wrappedPipelineReturn = {
updatePipeline: true,
},
downstream: {
+ retryable: true,
+ cancelable: false,
+ userPermissions: {
+ updatePipeline: true,
+ },
__typename: 'PipelineConnection',
nodes: [],
},
@@ -793,6 +825,11 @@ export const wrappedPipelineReturn = {
id: 'gid://gitlab/Ci::Pipeline/174',
iid: '37',
path: '/root/elemenohpee/-/pipelines/174',
+ retryable: true,
+ cancelable: false,
+ userPermissions: {
+ updatePipeline: true,
+ },
__typename: 'Pipeline',
status: {
__typename: 'DetailedStatus',
@@ -846,6 +883,7 @@ export const wrappedPipelineReturn = {
{
__typename: 'CiJob',
id: '83',
+ kind: BUILD_KIND,
name: 'build_n',
scheduledAt: null,
needs: {
@@ -916,3 +954,87 @@ export const mockCalloutsResponse = (mappedCallouts) => ({
},
},
});
+
+export const delayedJob = {
+ __typename: 'CiJob',
+ kind: BUILD_KIND,
+ name: 'delayed job',
+ scheduledAt: '2015-07-03T10:01:00.000Z',
+ needs: [],
+ status: {
+ __typename: 'DetailedStatus',
+ icon: 'status_scheduled',
+ tooltip: 'delayed manual action (%{remainingTime})',
+ hasDetails: true,
+ detailsPath: '/root/kinder-pipe/-/jobs/5339',
+ group: 'scheduled',
+ action: {
+ __typename: 'StatusAction',
+ icon: 'time-out',
+ title: 'Unschedule',
+ path: '/frontend-fixtures/builds-project/-/jobs/142/unschedule',
+ buttonTitle: 'Unschedule job',
+ },
+ },
+};
+
+export const mockJob = {
+ id: 4256,
+ name: 'test',
+ kind: BUILD_KIND,
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ tooltip: 'passed',
+ group: 'success',
+ detailsPath: '/root/ci-mock/builds/4256',
+ hasDetails: true,
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/ci-mock/builds/4256/retry',
+ method: 'post',
+ },
+ },
+};
+
+export const mockJobWithoutDetails = {
+ id: 4257,
+ name: 'job_without_details',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ detailsPath: '/root/ci-mock/builds/4257',
+ hasDetails: false,
+ },
+};
+
+export const mockJobWithUnauthorizedAction = {
+ id: 4258,
+ name: 'stop-environment',
+ status: {
+ icon: 'status_manual',
+ label: 'manual stop action (not allowed)',
+ tooltip: 'manual action',
+ group: 'manual',
+ detailsPath: '/root/ci-mock/builds/4258',
+ hasDetails: true,
+ action: null,
+ },
+};
+
+export const triggerJob = {
+ id: 4259,
+ name: 'trigger',
+ kind: BRIDGE_KIND,
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ action: null,
+ },
+};
diff --git a/spec/frontend/pipelines/graph_shared/links_inner_spec.js b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
index be422fac92c..2c6d126e12c 100644
--- a/spec/frontend/pipelines/graph_shared/links_inner_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { setHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
import { parseData } from '~/pipelines/components/parsing_utils';
import { createJobsHash } from '~/pipelines/utils';
@@ -42,7 +42,7 @@ describe('Links Inner component', () => {
// We create fixture so that each job has an empty div that represent
// the JobPill in the DOM. Each `JobPill` would have different coordinates,
// so we increment their coordinates on each iteration to simulate different positions.
- const setFixtures = ({ stages }) => {
+ const setHTMLFixtureLocal = ({ stages }) => {
const jobs = createJobsHash(stages);
const arrayOfJobs = Object.keys(jobs);
@@ -82,6 +82,7 @@ describe('Links Inner component', () => {
afterEach(() => {
jest.restoreAllMocks();
wrapper.destroy();
+ resetHTMLFixture();
});
describe('basic SVG creation', () => {
@@ -124,7 +125,7 @@ describe('Links Inner component', () => {
describe('with one need', () => {
beforeEach(() => {
- setFixtures(pipelineData);
+ setHTMLFixtureLocal(pipelineData);
createComponent({ pipelineData: pipelineData.stages });
});
@@ -143,7 +144,7 @@ describe('Links Inner component', () => {
describe('with a parallel need', () => {
beforeEach(() => {
- setFixtures(parallelNeedData);
+ setHTMLFixtureLocal(parallelNeedData);
createComponent({ pipelineData: parallelNeedData.stages });
});
@@ -162,7 +163,7 @@ describe('Links Inner component', () => {
describe('with same stage needs', () => {
beforeEach(() => {
- setFixtures(sameStageNeeds);
+ setHTMLFixtureLocal(sameStageNeeds);
createComponent({ pipelineData: sameStageNeeds.stages });
});
@@ -181,7 +182,7 @@ describe('Links Inner component', () => {
describe('with a large number of needs', () => {
beforeEach(() => {
- setFixtures(largePipelineData);
+ setHTMLFixtureLocal(largePipelineData);
createComponent({ pipelineData: largePipelineData.stages });
});
@@ -200,7 +201,7 @@ describe('Links Inner component', () => {
describe('interactions', () => {
beforeEach(() => {
- setFixtures(largePipelineData);
+ setHTMLFixtureLocal(largePipelineData);
createComponent({ pipelineData: largePipelineData.stages });
});
diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js
index c4639bd8e16..5cc11adf696 100644
--- a/spec/frontend/pipelines/header_component_spec.js
+++ b/spec/frontend/pipelines/header_component_spec.js
@@ -1,4 +1,4 @@
-import { GlModal, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlModal, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
@@ -21,6 +21,7 @@ describe('Pipeline details header', () => {
let glModalDirective;
let mutate = jest.fn();
+ const findAlert = () => wrapper.find(GlAlert);
const findDeleteModal = () => wrapper.find(GlModal);
const findRetryButton = () => wrapper.find('[data-testid="retryPipeline"]');
const findCancelButton = () => wrapper.find('[data-testid="cancelPipeline"]');
@@ -121,6 +122,22 @@ describe('Pipeline details header', () => {
it('should render retry action tooltip', () => {
expect(findRetryButton().attributes('title')).toBe(BUTTON_TOOLTIP_RETRY);
});
+
+ it('should display error message on failure', async () => {
+ const failureMessage = 'failure message';
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
+ data: {
+ pipelineRetry: {
+ errors: [failureMessage],
+ },
+ },
+ });
+
+ findRetryButton().vm.$emit('click');
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe(failureMessage);
+ });
});
describe('Retry action failed', () => {
@@ -156,6 +173,22 @@ describe('Pipeline details header', () => {
variables: { id: mockRunningPipelineHeader.id },
});
});
+
+ it('should display error message on failure', async () => {
+ const failureMessage = 'failure message';
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
+ data: {
+ pipelineCancel: {
+ errors: [failureMessage],
+ },
+ },
+ });
+
+ findCancelButton().vm.$emit('click');
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe(failureMessage);
+ });
});
describe('Delete action', () => {
@@ -179,6 +212,22 @@ describe('Pipeline details header', () => {
variables: { id: mockFailedPipelineHeader.id },
});
});
+
+ it('should display error message on failure', async () => {
+ const failureMessage = 'failure message';
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
+ data: {
+ pipelineDestroy: {
+ errors: [failureMessage],
+ },
+ },
+ });
+
+ findDeleteModal().vm.$emit('ok');
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe(failureMessage);
+ });
});
describe('Permissions', () => {
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js
index 59d4e808b32..57d1511d859 100644
--- a/spec/frontend/pipelines/mock_data.js
+++ b/spec/frontend/pipelines/mock_data.js
@@ -1141,3 +1141,218 @@ export const mockPipelineBranch = () => {
viewType: 'root',
};
};
+
+export const mockFailedJobsQueryResponse = {
+ data: {
+ project: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/20',
+ pipeline: {
+ __typename: 'Pipeline',
+ id: 'gid://gitlab/Ci::Pipeline/300',
+ jobs: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiJob',
+ status: 'FAILED',
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'failed-1848-1848',
+ detailsPath: '/root/ci-project/-/jobs/1848',
+ group: 'failed',
+ icon: 'status_failed',
+ label: 'failed',
+ text: 'failed',
+ tooltip: 'failed - (script failure)',
+ action: {
+ __typename: 'StatusAction',
+ id: 'Ci::Build-failed-1848',
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ method: 'post',
+ path: '/root/ci-project/-/jobs/1848/retry',
+ title: 'Retry',
+ },
+ },
+ id: 'gid://gitlab/Ci::Build/1848',
+ stage: {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/358',
+ name: 'build',
+ },
+ name: 'wait_job',
+ retryable: true,
+ userPermissions: {
+ __typename: 'JobPermissions',
+ readBuild: true,
+ updateBuild: true,
+ },
+ },
+ {
+ __typename: 'CiJob',
+ status: 'FAILED',
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'failed-1710-1710',
+ detailsPath: '/root/ci-project/-/jobs/1710',
+ group: 'failed',
+ icon: 'status_failed',
+ label: 'failed',
+ text: 'failed',
+ tooltip: 'failed - (script failure) (retried)',
+ action: null,
+ },
+ id: 'gid://gitlab/Ci::Build/1710',
+ stage: {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/358',
+ name: 'build',
+ },
+ name: 'wait_job',
+ retryable: false,
+ userPermissions: {
+ __typename: 'JobPermissions',
+ readBuild: true,
+ updateBuild: true,
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+};
+
+export const mockFailedJobsSummaryData = [
+ {
+ id: 1848,
+ failure: null,
+ failure_summary:
+ '<span>Pulling docker image node:latest ...<br/></span><span>Using docker image sha256:738d733448be00c72cb6618b7a06a1424806c6d239d8885e92f9b1e8727092b5 for node:latest with digest node@sha256:e5b7b349d517159246070bf14242027a9e220ffa8bd98a67ba1495d969c06c01 ...<br/></span><div class="section-start" data-timestamp="1651175313" data-section="prepare-script" role="button"></div><span class="term-fg-l-cyan term-bold section section-header js-s-prepare-script">Preparing environment</span><span class="section section-header js-s-prepare-script"><br/></span><span class="section line js-s-prepare-script">Running on runner-kvkqh24-project-20-concurrent-0 via 0706719b1b8d...<br/></span><div class="section-end" data-section="prepare-script"></div><div class="section-start" data-timestamp="1651175313" data-section="get-sources" role="button"></div><span class="term-fg-l-cyan term-bold section section-header js-s-get-sources">Getting source from Git repository</span><span class="section section-header js-s-get-sources"><br/></span><span class="term-fg-l-green term-bold section line js-s-get-sources">Fetching changes with git depth set to 50...</span><span class="section line js-s-get-sources"><br/>Reinitialized existing Git repository in /builds/root/ci-project/.git/<br/>fatal: couldn\'t find remote ref refs/heads/test<br/></span><div class="section-end" data-section="get-sources"></div><span class="term-fg-l-red term-bold">ERROR: Job failed: exit code 1<br/></span>',
+ },
+];
+
+export const mockFailedJobsData = [
+ {
+ normalizedId: 1848,
+ __typename: 'CiJob',
+ status: 'FAILED',
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'failed-1848-1848',
+ detailsPath: '/root/ci-project/-/jobs/1848',
+ group: 'failed',
+ icon: 'status_failed',
+ label: 'failed',
+ text: 'failed',
+ tooltip: 'failed - (script failure)',
+ action: {
+ __typename: 'StatusAction',
+ id: 'Ci::Build-failed-1848',
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ method: 'post',
+ path: '/root/ci-project/-/jobs/1848/retry',
+ title: 'Retry',
+ },
+ },
+ id: 'gid://gitlab/Ci::Build/1848',
+ stage: { __typename: 'CiStage', id: 'gid://gitlab/Ci::Stage/358', name: 'build' },
+ name: 'wait_job',
+ retryable: true,
+ userPermissions: { __typename: 'JobPermissions', readBuild: true, updateBuild: true },
+ },
+ {
+ normalizedId: 1710,
+ __typename: 'CiJob',
+ status: 'FAILED',
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'failed-1710-1710',
+ detailsPath: '/root/ci-project/-/jobs/1710',
+ group: 'failed',
+ icon: 'status_failed',
+ label: 'failed',
+ text: 'failed',
+ tooltip: 'failed - (script failure) (retried)',
+ action: null,
+ },
+ id: 'gid://gitlab/Ci::Build/1710',
+ stage: { __typename: 'CiStage', id: 'gid://gitlab/Ci::Stage/358', name: 'build' },
+ name: 'wait_job',
+ retryable: false,
+ userPermissions: { __typename: 'JobPermissions', readBuild: true, updateBuild: true },
+ },
+];
+
+export const mockPreparedFailedJobsData = [
+ {
+ __typename: 'CiJob',
+ _showDetails: true,
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ action: {
+ __typename: 'StatusAction',
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ id: 'Ci::Build-failed-1848',
+ method: 'post',
+ path: '/root/ci-project/-/jobs/1848/retry',
+ title: 'Retry',
+ },
+ detailsPath: '/root/ci-project/-/jobs/1848',
+ group: 'failed',
+ icon: 'status_failed',
+ id: 'failed-1848-1848',
+ label: 'failed',
+ text: 'failed',
+ tooltip: 'failed - (script failure)',
+ },
+ failure: null,
+ failureSummary:
+ '<span>Pulling docker image node:latest ...<br/></span><span>Using docker image sha256:738d733448be00c72cb6618b7a06a1424806c6d239d8885e92f9b1e8727092b5 for node:latest with digest node@sha256:e5b7b349d517159246070bf14242027a9e220ffa8bd98a67ba1495d969c06c01 ...<br/></span><div class="section-start" data-timestamp="1651175313" data-section="prepare-script" role="button"></div><span class="term-fg-l-cyan term-bold section section-header js-s-prepare-script">Preparing environment</span><span class="section section-header js-s-prepare-script"><br/></span><span class="section line js-s-prepare-script">Running on runner-kvkqh24-project-20-concurrent-0 via 0706719b1b8d...<br/></span><div class="section-end" data-section="prepare-script"></div><div class="section-start" data-timestamp="1651175313" data-section="get-sources" role="button"></div><span class="term-fg-l-cyan term-bold section section-header js-s-get-sources">Getting source from Git repository</span><span class="section section-header js-s-get-sources"><br/></span><span class="term-fg-l-green term-bold section line js-s-get-sources">Fetching changes with git depth set to 50...</span><span class="section line js-s-get-sources"><br/>Reinitialized existing Git repository in /builds/root/ci-project/.git/<br/>fatal: couldn\'t find remote ref refs/heads/test<br/></span><div class="section-end" data-section="get-sources"></div><span class="term-fg-l-red term-bold">ERROR: Job failed: exit code 1<br/></span>',
+ id: 'gid://gitlab/Ci::Build/1848',
+ name: 'wait_job',
+ normalizedId: 1848,
+ retryable: true,
+ stage: { __typename: 'CiStage', id: 'gid://gitlab/Ci::Stage/358', name: 'build' },
+ status: 'FAILED',
+ userPermissions: { __typename: 'JobPermissions', readBuild: true, updateBuild: true },
+ },
+];
+
+export const mockPreparedFailedJobsDataNoPermission = [
+ {
+ ...mockPreparedFailedJobsData[0],
+ userPermissions: { __typename: 'JobPermissions', readBuild: false, updateBuild: false },
+ },
+];
+
+export const successRetryMutationResponse = {
+ data: {
+ jobRetry: {
+ job: {
+ __typename: 'CiJob',
+ id: '"gid://gitlab/Ci::Build/1985"',
+ detailedStatus: {
+ detailsPath: '/root/project/-/jobs/1985',
+ id: 'pending-1985-1985',
+ __typename: 'DetailedStatus',
+ },
+ },
+ errors: [],
+ __typename: 'JobRetryPayload',
+ },
+ },
+};
+
+export const failedRetryMutationResponse = {
+ data: {
+ jobRetry: {
+ job: {},
+ errors: ['New Error'],
+ __typename: 'JobRetryPayload',
+ },
+ },
+};
diff --git a/spec/frontend/pipelines/pipeline_graph/utils_spec.js b/spec/frontend/pipelines/pipeline_graph/utils_spec.js
index 5816bc06fe3..d6b13da3c3a 100644
--- a/spec/frontend/pipelines/pipeline_graph/utils_spec.js
+++ b/spec/frontend/pipelines/pipeline_graph/utils_spec.js
@@ -1,4 +1,5 @@
-import { createJobsHash, generateJobNeedsDict } from '~/pipelines/utils';
+import { createJobsHash, generateJobNeedsDict, getPipelineDefaultTab } from '~/pipelines/utils';
+import { TAB_QUERY_PARAM, validPipelineTabNames } from '~/pipelines/constants';
describe('utils functions', () => {
const jobName1 = 'build_1';
@@ -169,4 +170,21 @@ describe('utils functions', () => {
});
});
});
+
+ describe('getPipelineDefaultTab', () => {
+ const baseUrl = 'http://gitlab.com/user/multi-projects-small/-/pipelines/332/';
+ it('returns null if there was no `tab` params', () => {
+ expect(getPipelineDefaultTab(baseUrl)).toBe(null);
+ });
+
+ it('returns null if there was no valid tab param', () => {
+ expect(getPipelineDefaultTab(`${baseUrl}?${TAB_QUERY_PARAM}=invalid`)).toBe(null);
+ });
+
+ it('returns the correct tab name if present', () => {
+ validPipelineTabNames.forEach((tabName) => {
+ expect(getPipelineDefaultTab(`${baseUrl}?${TAB_QUERY_PARAM}=${tabName}`)).toBe(tabName);
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index d2b30c93746..de9f394db43 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -82,6 +82,8 @@ describe('Pipelines', () => {
provide: {
pipelineEditorPath: '',
suggestedCiTemplates: [],
+ ciRunnerSettingsPath: paths.ciRunnerSettingsPath,
+ anyRunnersAvailable: true,
},
propsData: {
store: new Store(),
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
index d5acb115bc1..74a9d8c354f 100644
--- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
@@ -82,17 +82,16 @@ describe('Actions TestReports Store', () => {
);
});
- it('should create flash on API error', async () => {
+ it('should call SET_SUITE_ERROR on error', () => {
const index = 0;
- await testAction(
+ return testAction(
actions.fetchTestSuite,
index,
{ ...state, testReports, suiteEndpoint: null },
- [],
+ [{ type: types.SET_SUITE_ERROR, payload: expect.any(Error) }],
[{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
);
- expect(createFlash).toHaveBeenCalled();
});
describe('when we already have the suite data', () => {
diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
index f2dbeec6a06..6ab479a257c 100644
--- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
@@ -1,6 +1,9 @@
import testReports from 'test_fixtures/pipelines/test_report.json';
import * as types from '~/pipelines/stores/test_reports/mutation_types';
import mutations from '~/pipelines/stores/test_reports/mutations';
+import createFlash from '~/flash';
+
+jest.mock('~/flash.js');
describe('Mutations TestReports Store', () => {
let mockState;
@@ -44,6 +47,24 @@ describe('Mutations TestReports Store', () => {
});
});
+ describe('set suite error', () => {
+ it('should set the error message in state if provided', () => {
+ const message = 'Test report artifacts have expired';
+
+ mutations[types.SET_SUITE_ERROR](mockState, {
+ response: { data: { errors: message } },
+ });
+
+ expect(mockState.errorMessage).toBe(message);
+ });
+
+ it('should show a flash message otherwise', () => {
+ mutations[types.SET_SUITE_ERROR](mockState, {});
+
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
+
describe('set selected suite index', () => {
it('should set selectedSuiteIndex', () => {
const selectedSuiteIndex = 0;
diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
index 97241e14129..dc72fa31ace 100644
--- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
@@ -1,12 +1,13 @@
import { GlButton, GlFriendlyWrap, GlLink, GlPagination } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import testReports from 'test_fixtures/pipelines/test_report.json';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue';
import { TestStatus } from '~/pipelines/constants';
import * as getters from '~/pipelines/stores/test_reports/getters';
import { formatFilePath } from '~/pipelines/stores/test_reports/utils';
+import { ARTIFACTS_EXPIRED_ERROR_MESSAGE } from '~/pipelines/stores/test_reports/constants';
import skippedTestCases from './mock_data';
Vue.use(Vuex);
@@ -23,13 +24,14 @@ describe('Test reports suite table', () => {
const testCases = testSuite.test_cases;
const blobPath = '/test/blob/path';
- const noCasesMessage = () => wrapper.find('.js-no-test-cases');
- const allCaseRows = () => wrapper.findAll('.js-case-row');
- const findCaseRowAtIndex = (index) => wrapper.findAll('.js-case-row').at(index);
+ const noCasesMessage = () => wrapper.findByTestId('no-test-cases');
+ const artifactsExpiredMessage = () => wrapper.findByTestId('artifacts-expired');
+ const allCaseRows = () => wrapper.findAllByTestId('test-case-row');
+ const findCaseRowAtIndex = (index) => wrapper.findAllByTestId('test-case-row').at(index);
const findLinkForRow = (row) => row.find(GlLink);
const findIconForRow = (row, status) => row.find(`.ci-status-icon-${status}`);
- const createComponent = (suite = testSuite, perPage = 20) => {
+ const createComponent = ({ suite = testSuite, perPage = 20, errorMessage } = {}) => {
store = new Vuex.Store({
state: {
blobPath,
@@ -41,11 +43,12 @@ describe('Test reports suite table', () => {
page: 1,
perPage,
},
+ errorMessage,
},
getters,
});
- wrapper = shallowMount(SuiteTable, {
+ wrapper = shallowMountExtended(SuiteTable, {
store,
stubs: { GlFriendlyWrap },
});
@@ -55,12 +58,18 @@ describe('Test reports suite table', () => {
wrapper.destroy();
});
- describe('should not render', () => {
- beforeEach(() => createComponent([]));
+ it('should render a message when there are no test cases', () => {
+ createComponent({ suite: [] });
- it('a table when there are no test cases', () => {
- expect(noCasesMessage().exists()).toBe(true);
- });
+ expect(noCasesMessage().exists()).toBe(true);
+ expect(artifactsExpiredMessage().exists()).toBe(false);
+ });
+
+ it('should render a message when artifacts have expired', () => {
+ createComponent({ suite: [], errorMessage: ARTIFACTS_EXPIRED_ERROR_MESSAGE });
+
+ expect(noCasesMessage().exists()).toBe(true);
+ expect(artifactsExpiredMessage().exists()).toBe(true);
});
describe('when a test suite is supplied', () => {
@@ -102,7 +111,7 @@ describe('Test reports suite table', () => {
const perPage = 2;
beforeEach(() => {
- createComponent(testSuite, perPage);
+ createComponent({ testSuite, perPage });
});
it('renders one page of test cases', () => {
@@ -117,11 +126,13 @@ describe('Test reports suite table', () => {
describe('when a test case classname property is null', () => {
it('still renders all test cases', () => {
createComponent({
- ...testSuite,
- test_cases: testSuite.test_cases.map((testCase) => ({
- ...testCase,
- classname: null,
- })),
+ testSuite: {
+ ...testSuite,
+ test_cases: testSuite.test_cases.map((testCase) => ({
+ ...testCase,
+ classname: null,
+ })),
+ },
});
expect(allCaseRows()).toHaveLength(testCases.length);
@@ -131,11 +142,13 @@ describe('Test reports suite table', () => {
describe('when a test case name property is null', () => {
it('still renders all test cases', () => {
createComponent({
- ...testSuite,
- test_cases: testSuite.test_cases.map((testCase) => ({
- ...testCase,
- name: null,
- })),
+ testSuite: {
+ ...testSuite,
+ test_cases: testSuite.test_cases.map((testCase) => ({
+ ...testCase,
+ name: null,
+ })),
+ },
});
expect(allCaseRows()).toHaveLength(testCases.length);
diff --git a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
index 42ae154fb5e..ba478363d04 100644
--- a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
@@ -34,6 +34,7 @@ describe('Pipeline Branch Name Token', () => {
value: {
data: '',
},
+ cursorPosition: 'start',
};
const optionsWithDefaultBranchName = (options) => {
diff --git a/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js
index 684d2d0664a..b8abf2c1727 100644
--- a/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js
@@ -20,6 +20,7 @@ describe('Pipeline Source Token', () => {
value: {
data: '',
},
+ cursorPosition: 'start',
};
const createComponent = () => {
diff --git a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
index 1db736ba01e..2c5fa8b00e2 100644
--- a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
@@ -20,6 +20,7 @@ describe('Pipeline Status Token', () => {
value: {
data: '',
},
+ cursorPosition: 'start',
};
const createComponent = () => {
diff --git a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
index b03dbb73b95..596a9218c39 100644
--- a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
@@ -29,6 +29,7 @@ describe('Pipeline Branch Name Token', () => {
value: {
data: '',
},
+ cursorPosition: 'start',
};
const createComponent = (options, data) => {
diff --git a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
index 7ddbbb3b005..397dbdf95a9 100644
--- a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
@@ -24,6 +24,7 @@ describe('Pipeline Trigger Author Token', () => {
value: {
data: '',
},
+ cursorPosition: 'start',
};
const createComponent = (data) => {