summaryrefslogtreecommitdiff
path: root/spec/frontend/pipelines
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-02-18 10:34:06 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-02-18 10:34:06 +0000
commit859a6fb938bb9ee2a317c46dfa4fcc1af49608f0 (patch)
treed7f2700abe6b4ffcb2dcfc80631b2d87d0609239 /spec/frontend/pipelines
parent446d496a6d000c73a304be52587cd9bbc7493136 (diff)
downloadgitlab-ce-859a6fb938bb9ee2a317c46dfa4fcc1af49608f0.tar.gz
Add latest changes from gitlab-org/gitlab@13-9-stable-eev13.9.0-rc42
Diffstat (limited to 'spec/frontend/pipelines')
-rw-r--r--spec/frontend/pipelines/blank_state_spec.js21
-rw-r--r--spec/frontend/pipelines/components/dag/dag_annotations_spec.js2
-rw-r--r--spec/frontend/pipelines/components/dag/dag_graph_spec.js4
-rw-r--r--spec/frontend/pipelines/components/dag/dag_spec.js6
-rw-r--r--spec/frontend/pipelines/components/dag/parsing_utils_spec.js2
-rw-r--r--spec/frontend/pipelines/components/pipelines_filtered_search_spec.js2
-rw-r--r--spec/frontend/pipelines/empty_state_spec.js105
-rw-r--r--spec/frontend/pipelines/graph/action_component_spec.js2
-rw-r--r--spec/frontend/pipelines/graph/graph_component_legacy_spec.js12
-rw-r--r--spec/frontend/pipelines/graph/graph_component_spec.js16
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js6
-rw-r--r--spec/frontend/pipelines/graph/job_name_component_spec.js3
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js9
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_column_legacy_spec.js4
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_column_spec.js113
-rw-r--r--spec/frontend/pipelines/graph/stage_column_component_spec.js4
-rw-r--r--spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap23
-rw-r--r--spec/frontend/pipelines/graph_shared/links_inner_spec.js197
-rw-r--r--spec/frontend/pipelines/graph_shared/links_layer_spec.js (renamed from spec/frontend/pipelines/shared/links_layer_spec.js)4
-rw-r--r--spec/frontend/pipelines/header_component_spec.js10
-rw-r--r--spec/frontend/pipelines/legacy_header_component_spec.js116
-rw-r--r--spec/frontend/pipelines/pipeline_graph/mock_data.js135
-rw-r--r--spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js8
-rw-r--r--spec/frontend/pipelines/pipeline_triggerer_spec.js2
-rw-r--r--spec/frontend/pipelines/pipeline_url_spec.js28
-rw-r--r--spec/frontend/pipelines/pipelines_actions_spec.js66
-rw-r--r--spec/frontend/pipelines/pipelines_artifacts_spec.js2
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js990
-rw-r--r--spec/frontend/pipelines/pipelines_table_row_spec.js4
-rw-r--r--spec/frontend/pipelines/stage_spec.js278
-rw-r--r--spec/frontend/pipelines/test_reports/stores/actions_spec.js7
-rw-r--r--spec/frontend/pipelines/test_reports/stores/getters_spec.js73
-rw-r--r--spec/frontend/pipelines/test_reports/stores/utils_spec.js16
-rw-r--r--spec/frontend/pipelines/test_reports/test_case_details_spec.js37
-rw-r--r--spec/frontend/pipelines/test_reports/test_reports_spec.js2
-rw-r--r--spec/frontend/pipelines/test_reports/test_suite_table_spec.js44
-rw-r--r--spec/frontend/pipelines/test_reports/test_summary_table_spec.js2
-rw-r--r--spec/frontend/pipelines/time_ago_spec.js2
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js2
39 files changed, 1459 insertions, 900 deletions
diff --git a/spec/frontend/pipelines/blank_state_spec.js b/spec/frontend/pipelines/blank_state_spec.js
index c09d9232569..5dcf3d267ed 100644
--- a/spec/frontend/pipelines/blank_state_spec.js
+++ b/spec/frontend/pipelines/blank_state_spec.js
@@ -1,25 +1,20 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import component from '~/pipelines/components/pipelines_list/blank_state.vue';
+import { getByText } from '@testing-library/dom';
+import { mount } from '@vue/test-utils';
+import BlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
describe('Pipelines Blank State', () => {
- let vm;
- let Component;
-
- beforeEach(() => {
- Component = Vue.extend(component);
-
- vm = mountComponent(Component, {
+ const wrapper = mount(BlankState, {
+ propsData: {
svgPath: 'foo',
message: 'Blank State',
- });
+ },
});
it('should render svg', () => {
- expect(vm.$el.querySelector('.svg-content img').getAttribute('src')).toEqual('foo');
+ expect(wrapper.find('.svg-content img').attributes('src')).toEqual('foo');
});
it('should render message', () => {
- expect(vm.$el.querySelector('h4').textContent.trim()).toEqual('Blank State');
+ expect(getByText(wrapper.element, /Blank State/i)).toBeTruthy();
});
});
diff --git a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
index 80807c0b330..1941a7f2777 100644
--- a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount, mount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
import DagAnnotations from '~/pipelines/components/dag/dag_annotations.vue';
import { singleNote, multiNote } from './mock_data';
diff --git a/spec/frontend/pipelines/components/dag/dag_graph_spec.js b/spec/frontend/pipelines/components/dag/dag_graph_spec.js
index ccfb2ae7cee..4619548d1bb 100644
--- a/spec/frontend/pipelines/components/dag/dag_graph_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_graph_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
-import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
import { IS_HIGHLIGHTED, LINK_SELECTOR, NODE_SELECTOR } from '~/pipelines/components/dag/constants';
-import { highlightIn, highlightOut } from '~/pipelines/components/dag/interactions';
+import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
import { createSankey } from '~/pipelines/components/dag/drawing_utils';
+import { highlightIn, highlightOut } from '~/pipelines/components/dag/interactions';
import { removeOrphanNodes } from '~/pipelines/components/parsing_utils';
import { parsedData } from './mock_data';
diff --git a/spec/frontend/pipelines/components/dag/dag_spec.js b/spec/frontend/pipelines/components/dag/dag_spec.js
index f6195e30e44..14030930657 100644
--- a/spec/frontend/pipelines/components/dag/dag_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_spec.js
@@ -1,10 +1,10 @@
-import { mount, shallowMount } from '@vue/test-utils';
import { GlAlert, GlEmptyState } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from '~/pipelines/components/dag/constants';
import Dag from '~/pipelines/components/dag/dag.vue';
-import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
import DagAnnotations from '~/pipelines/components/dag/dag_annotations.vue';
+import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
-import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from '~/pipelines/components/dag/constants';
import { PARSE_FAILURE, UNSUPPORTED_DATA } from '~/pipelines/constants';
import {
mockParsedGraphQLNodes,
diff --git a/spec/frontend/pipelines/components/dag/parsing_utils_spec.js b/spec/frontend/pipelines/components/dag/parsing_utils_spec.js
index 5d3f680a57c..84ff83883b7 100644
--- a/spec/frontend/pipelines/components/dag/parsing_utils_spec.js
+++ b/spec/frontend/pipelines/components/dag/parsing_utils_spec.js
@@ -1,3 +1,4 @@
+import { createSankey } from '~/pipelines/components/dag/drawing_utils';
import {
createNodeDict,
makeLinksFromNodes,
@@ -7,7 +8,6 @@ import {
getMaxNodes,
} from '~/pipelines/components/parsing_utils';
-import { createSankey } from '~/pipelines/components/dag/drawing_utils';
import { mockParsedGraphQLNodes } from './mock_data';
describe('DAG visualization parsing utilities', () => {
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
index 00fe9e784b3..e43aa2a02f5 100644
--- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
@@ -1,6 +1,6 @@
+import { GlFilteredSearch } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { GlFilteredSearch } from '@gitlab/ui';
import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import PipelinesFilteredSearch from '~/pipelines/components/pipelines_list/pipelines_filtered_search.vue';
diff --git a/spec/frontend/pipelines/empty_state_spec.js b/spec/frontend/pipelines/empty_state_spec.js
index 7e42a3b5ae9..3ebedc9ac87 100644
--- a/spec/frontend/pipelines/empty_state_spec.js
+++ b/spec/frontend/pipelines/empty_state_spec.js
@@ -1,7 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { withGonExperiment } from 'helpers/experimentation_helper';
import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue';
-import Tracking from '~/tracking';
describe('Pipelines Empty State', () => {
let wrapper;
@@ -40,104 +38,15 @@ describe('Pipelines Empty State', () => {
expect(findGetStartedButton().attributes('href')).toBe('foo');
});
- describe('when in control group', () => {
- it('should render empty state information', () => {
- expect(findInfoText()).toContain(
- 'Continuous Integration can help catch bugs by running your tests automatically',
- 'while Continuous Deployment can help you deliver code to your product environment',
- );
- });
-
- it('should render a button', () => {
- expect(findGetStartedButton().text()).toBe('Get started with Pipelines');
- });
- });
-
- describe('when in experiment group', () => {
- withGonExperiment('pipelinesEmptyState');
-
- beforeEach(() => {
- createWrapper();
- });
-
- it('should render empty state information', () => {
- expect(findInfoText()).toContain(
- 'GitLab CI/CD can automatically build, test, and deploy your code. Let GitLab take care of time',
- 'consuming tasks, so you can spend more time creating',
- );
- });
-
- it('should render button text', () => {
- expect(findGetStartedButton().text()).toBe('Get started with CI/CD');
- });
+ it('should render empty state information', () => {
+ expect(findInfoText()).toContain(
+ 'GitLab CI/CD can automatically build, test, and deploy your code. Let GitLab take care of time',
+ 'consuming tasks, so you can spend more time creating',
+ );
});
- describe('tracking', () => {
- let origGon;
-
- describe('when data is set', () => {
- beforeEach(() => {
- jest.spyOn(Tracking, 'event').mockImplementation(() => {});
- origGon = window.gon;
-
- window.gon = {
- tracking_data: {
- category: 'Growth::Activation::Experiment::PipelinesEmptyState',
- value: 1,
- property: 'experimental_group',
- label: 'label',
- },
- };
- createWrapper();
- });
-
- afterEach(() => {
- window.gon = origGon;
- });
-
- it('tracks when mounted', () => {
- expect(Tracking.event).toHaveBeenCalledWith(
- 'Growth::Activation::Experiment::PipelinesEmptyState',
- 'viewed',
- {
- value: 1,
- label: 'label',
- property: 'experimental_group',
- },
- );
- });
-
- it('tracks when button is clicked', () => {
- findGetStartedButton().vm.$emit('click');
-
- expect(Tracking.event).toHaveBeenCalledWith(
- 'Growth::Activation::Experiment::PipelinesEmptyState',
- 'documentation_clicked',
- {
- value: 1,
- label: 'label',
- property: 'experimental_group',
- },
- );
- });
- });
-
- describe('when no data is defined', () => {
- beforeEach(() => {
- jest.spyOn(Tracking, 'event').mockImplementation(() => {});
-
- createWrapper();
- });
-
- it('does not track on view', () => {
- expect(Tracking.event).not.toHaveBeenCalled();
- });
-
- it('does not track when button is clicked', () => {
- findGetStartedButton().vm.$emit('click');
- expect(Tracking.event).not.toHaveBeenCalled();
- });
- });
+ it('should render button text', () => {
+ expect(findGetStartedButton().text()).toBe('Get started with CI/CD');
});
});
});
diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js
index 95d96e127c6..6a7018fa1e5 100644
--- a/spec/frontend/pipelines/graph/action_component_spec.js
+++ b/spec/frontend/pipelines/graph/action_component_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
diff --git a/spec/frontend/pipelines/graph/graph_component_legacy_spec.js b/spec/frontend/pipelines/graph/graph_component_legacy_spec.js
index 840b1f8baf5..a955572a481 100644
--- a/spec/frontend/pipelines/graph/graph_component_legacy_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_legacy_spec.js
@@ -1,14 +1,14 @@
-import { nextTick } from 'vue';
-import { mount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { setHTMLFixture } from 'helpers/fixtures';
-import PipelineStore from '~/pipelines/stores/pipeline_store';
import GraphComponentLegacy from '~/pipelines/components/graph/graph_component_legacy.vue';
-import StageColumnComponentLegacy from '~/pipelines/components/graph/stage_column_component_legacy.vue';
import LinkedPipelinesColumnLegacy from '~/pipelines/components/graph/linked_pipelines_column_legacy.vue';
-import graphJSON from './mock_data_legacy';
-import linkedPipelineJSON from './linked_pipelines_mock_data';
+import StageColumnComponentLegacy from '~/pipelines/components/graph/stage_column_component_legacy.vue';
import PipelinesMediator from '~/pipelines/pipeline_details_mediator';
+import PipelineStore from '~/pipelines/stores/pipeline_store';
+import linkedPipelineJSON from './linked_pipelines_mock_data';
+import graphJSON from './mock_data_legacy';
describe('graph component', () => {
let store;
diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js
index cfc3b7af282..3e8d4ba314c 100644
--- a/spec/frontend/pipelines/graph/graph_component_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_spec.js
@@ -1,10 +1,10 @@
import { mount, shallowMount } from '@vue/test-utils';
+import { GRAPHQL } from '~/pipelines/components/graph/constants';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
-import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
import JobItem from '~/pipelines/components/graph/job_item.vue';
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
+import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
-import { GRAPHQL } from '~/pipelines/components/graph/constants';
import {
generateResponse,
mockPipelineResponse,
@@ -22,6 +22,13 @@ describe('graph component', () => {
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
};
+ const defaultData = {
+ measurements: {
+ width: 800,
+ height: 800,
+ },
+ };
+
const createComponent = ({
data = {},
mountFn = shallowMount,
@@ -34,7 +41,10 @@ describe('graph component', () => {
...props,
},
data() {
- return { ...data };
+ return {
+ ...defaultData,
+ ...data,
+ };
},
provide: {
dataMethod: GRAPHQL,
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 54593c527cb..202365ecd35 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -1,11 +1,11 @@
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { shallowMount } from '@vue/test-utils';
-import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
-import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
+import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
import { mockPipelineResponse } from './mock_data';
const defaultProvide = {
diff --git a/spec/frontend/pipelines/graph/job_name_component_spec.js b/spec/frontend/pipelines/graph/job_name_component_spec.js
index f0aa646b8d7..658b5be87d4 100644
--- a/spec/frontend/pipelines/graph/job_name_component_spec.js
+++ b/spec/frontend/pipelines/graph/job_name_component_spec.js
@@ -1,7 +1,6 @@
import { mount } from '@vue/test-utils';
-import ciIcon from '~/vue_shared/components/ci_icon.vue';
-
import jobNameComponent from '~/pipelines/components/graph/job_name_component.vue';
+import ciIcon from '~/vue_shared/components/ci_icon.vue';
describe('job name component', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index fb005d628a9..96f2cd1e371 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -1,9 +1,10 @@
-import { mount } from '@vue/test-utils';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
+import { UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import mockData from './linked_pipelines_mock_data';
-import { UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
const mockPipeline = mockData.triggered[0];
const validTriggeredPipelineId = mockPipeline.project.id;
@@ -212,11 +213,11 @@ describe('Linked pipeline', () => {
expect(wrapper.emitted().pipelineClicked).toBeTruthy();
});
- it('should emit `bv::hide::tooltip` to close the tooltip', () => {
+ it(`should emit ${BV_HIDE_TOOLTIP} to close the tooltip`, () => {
jest.spyOn(wrapper.vm.$root, '$emit');
findButton().trigger('click');
- expect(wrapper.vm.$root.$emit.mock.calls[0]).toEqual(['bv::hide::tooltip']);
+ expect(wrapper.vm.$root.$emit.mock.calls[0]).toEqual([BV_HIDE_TOOLTIP]);
});
it('should emit downstreamHovered with job name on mouseover', () => {
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_legacy_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_legacy_spec.js
index b6c700c65d2..200e3f48401 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_column_legacy_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_column_legacy_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import LinkedPipelinesColumnLegacy from '~/pipelines/components/graph/linked_pipelines_column_legacy.vue';
-import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue';
import { UPSTREAM } from '~/pipelines/components/graph/constants';
+import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue';
+import LinkedPipelinesColumnLegacy from '~/pipelines/components/graph/linked_pipelines_column_legacy.vue';
import mockData from './linked_pipelines_mock_data';
describe('Linked Pipelines Column', () => {
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
index 6db152f2607..8f01accccc1 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
@@ -1,11 +1,11 @@
-import VueApollo from 'vue-apollo';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
+import { DOWNSTREAM, GRAPHQL, UPSTREAM } from '~/pipelines/components/graph/constants';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
-import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue';
-import { DOWNSTREAM, GRAPHQL } from '~/pipelines/components/graph/constants';
+import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
import { LOAD_FAILURE } from '~/pipelines/constants';
import {
mockPipelineResponse,
@@ -17,7 +17,7 @@ const processedPipeline = pipelineWithUpstreamDownstream(mockPipelineResponse);
describe('Linked Pipelines Column', () => {
const defaultProps = {
- columnTitle: 'Upstream',
+ columnTitle: 'Downstream',
linkedPipelines: processedPipeline.downstream,
type: DOWNSTREAM,
};
@@ -45,14 +45,15 @@ describe('Linked Pipelines Column', () => {
});
};
- const createComponentWithApollo = (
+ const createComponentWithApollo = ({
mountFn = shallowMount,
getPipelineDetailsHandler = jest.fn().mockResolvedValue(wrappedPipelineReturn),
- ) => {
+ props = {},
+ } = {}) => {
const requestHandlers = [[getPipelineDetails, getPipelineDetailsHandler]];
const apolloProvider = createMockApollo(requestHandlers);
- createComponent({ apolloProvider, mountFn });
+ createComponent({ apolloProvider, mountFn, props });
};
afterEach(() => {
@@ -86,34 +87,90 @@ describe('Linked Pipelines Column', () => {
await wrapper.vm.$nextTick();
};
- describe('when successful', () => {
- beforeEach(() => {
- createComponentWithApollo(mount);
+ describe('downstream', () => {
+ describe('when successful', () => {
+ beforeEach(() => {
+ createComponentWithApollo({ mountFn: mount });
+ });
+
+ it('toggles the pipeline visibility', async () => {
+ expect(findPipelineGraph().exists()).toBe(false);
+ await clickExpandButtonAndAwaitTimers();
+ expect(findPipelineGraph().exists()).toBe(true);
+ await clickExpandButton();
+ expect(findPipelineGraph().exists()).toBe(false);
+ });
});
- it('toggles the pipeline visibility', async () => {
- expect(findPipelineGraph().exists()).toBe(false);
- await clickExpandButtonAndAwaitTimers();
- expect(findPipelineGraph().exists()).toBe(true);
- await clickExpandButton();
- expect(findPipelineGraph().exists()).toBe(false);
+ describe('on error', () => {
+ beforeEach(() => {
+ createComponentWithApollo({
+ mountFn: mount,
+ getPipelineDetailsHandler: jest.fn().mockRejectedValue(new Error('GraphQL error')),
+ });
+ });
+
+ it('emits the error', async () => {
+ await clickExpandButton();
+ expect(wrapper.emitted().error).toEqual([[LOAD_FAILURE]]);
+ });
+
+ it('does not show the pipeline', async () => {
+ expect(findPipelineGraph().exists()).toBe(false);
+ await clickExpandButtonAndAwaitTimers();
+ expect(findPipelineGraph().exists()).toBe(false);
+ });
});
});
- describe('on error', () => {
- beforeEach(() => {
- createComponentWithApollo(mount, jest.fn().mockRejectedValue(new Error('GraphQL error')));
- });
-
- it('emits the error', async () => {
- await clickExpandButton();
- expect(wrapper.emitted().error).toEqual([[LOAD_FAILURE]]);
+ describe('upstream', () => {
+ const upstreamProps = {
+ columnTitle: 'Upstream',
+ /*
+ Because the IDs need to match to work, rather
+ than make new mock data, we are representing
+ the upstream pipeline with the downstream data.
+ */
+ linkedPipelines: processedPipeline.downstream,
+ type: UPSTREAM,
+ };
+
+ describe('when successful', () => {
+ beforeEach(() => {
+ createComponentWithApollo({
+ mountFn: mount,
+ props: upstreamProps,
+ });
+ });
+
+ it('toggles the pipeline visibility', async () => {
+ expect(findPipelineGraph().exists()).toBe(false);
+ await clickExpandButtonAndAwaitTimers();
+ expect(findPipelineGraph().exists()).toBe(true);
+ await clickExpandButton();
+ expect(findPipelineGraph().exists()).toBe(false);
+ });
});
- it('does not show the pipeline', async () => {
- expect(findPipelineGraph().exists()).toBe(false);
- await clickExpandButtonAndAwaitTimers();
- expect(findPipelineGraph().exists()).toBe(false);
+ describe('on error', () => {
+ beforeEach(() => {
+ createComponentWithApollo({
+ mountFn: mount,
+ getPipelineDetailsHandler: jest.fn().mockRejectedValue(new Error('GraphQL error')),
+ props: upstreamProps,
+ });
+ });
+
+ it('emits the error', async () => {
+ await clickExpandButton();
+ expect(wrapper.emitted().error).toEqual([[LOAD_FAILURE]]);
+ });
+
+ it('does not show the pipeline', async () => {
+ expect(findPipelineGraph().exists()).toBe(false);
+ await clickExpandButtonAndAwaitTimers();
+ expect(findPipelineGraph().exists()).toBe(false);
+ });
});
});
});
diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/pipelines/graph/stage_column_component_spec.js
index 202e25ccda3..16dc70a63a5 100644
--- a/spec/frontend/pipelines/graph/stage_column_component_spec.js
+++ b/spec/frontend/pipelines/graph/stage_column_component_spec.js
@@ -68,6 +68,10 @@ describe('stage column component', () => {
it('should render the provided groups', () => {
expect(findAllStageColumnGroups().length).toBe(mockGroups.length);
});
+
+ it('should emit updateMeasurements event on mount', () => {
+ expect(wrapper.emitted().updateMeasurements).toHaveLength(1);
+ });
});
describe('when job notifies action is complete', () => {
diff --git a/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap b/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap
new file mode 100644
index 00000000000..cf2b66dea5f
--- /dev/null
+++ b/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Links Inner component with a large number of needs matches snapshot and has expected path 1`] = `
+"<div class=\\"gl-display-flex gl-relative\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
+ <path d=\\"M202,118L42,118C72,118,72,138,102,138\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M202,118L52,118C82,118,82,148,112,148\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M222,138L62,138C92,138,92,158,122,158\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M212,128L72,128C102,128,102,168,132,168\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M232,148L82,148C112,148,112,178,142,178\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ </svg> </div>"
+`;
+
+exports[`Links Inner component with a parallel need matches snapshot and has expected path 1`] = `
+"<div class=\\"gl-display-flex gl-relative\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
+ <path d=\\"M192,108L22,108C52,108,52,118,82,118\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ </svg> </div>"
+`;
+
+exports[`Links Inner component with one need matches snapshot and has expected path 1`] = `
+"<div class=\\"gl-display-flex gl-relative\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
+ <path d=\\"M202,118L42,118C72,118,72,138,102,138\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ </svg> </div>"
+`;
diff --git a/spec/frontend/pipelines/graph_shared/links_inner_spec.js b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
new file mode 100644
index 00000000000..6cabe2bc8a7
--- /dev/null
+++ b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
@@ -0,0 +1,197 @@
+import { shallowMount } from '@vue/test-utils';
+import { setHTMLFixture } from 'helpers/fixtures';
+import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
+import { createJobsHash } from '~/pipelines/utils';
+import {
+ jobRect,
+ largePipelineData,
+ parallelNeedData,
+ pipelineData,
+ pipelineDataWithNoNeeds,
+ rootRect,
+} from '../pipeline_graph/mock_data';
+
+describe('Links Inner component', () => {
+ const containerId = 'pipeline-graph-container';
+ const defaultProps = {
+ containerId,
+ containerMeasurements: { width: 1019, height: 445 },
+ pipelineId: 1,
+ pipelineData: [],
+ };
+ let wrapper;
+
+ const createComponent = (props) => {
+ wrapper = shallowMount(LinksInner, {
+ propsData: { ...defaultProps, ...props },
+ });
+ };
+
+ const findLinkSvg = () => wrapper.find('#link-svg');
+ const findAllLinksPath = () => findLinkSvg().findAll('path');
+
+ // 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 simulat different positions.
+ const setFixtures = ({ stages }) => {
+ const jobs = createJobsHash(stages);
+ const arrayOfJobs = Object.keys(jobs);
+
+ const linksHtmlElements = arrayOfJobs.map((job) => {
+ return `<div id=${job}-${defaultProps.pipelineId} />`;
+ });
+
+ setHTMLFixture(`<div id="${containerId}">${linksHtmlElements.join(' ')}</div>`);
+
+ // We are mocking the clientRect data of each job and the container ID.
+ jest
+ .spyOn(document.getElementById(containerId), 'getBoundingClientRect')
+ .mockImplementation(() => rootRect);
+
+ arrayOfJobs.forEach((job, index) => {
+ jest
+ .spyOn(
+ document.getElementById(`${job}-${defaultProps.pipelineId}`),
+ 'getBoundingClientRect',
+ )
+ .mockImplementation(() => {
+ const newValue = 10 * index;
+ const { left, right, top, bottom, x, y } = jobRect;
+ return {
+ ...jobRect,
+ left: left + newValue,
+ right: right + newValue,
+ top: top + newValue,
+ bottom: bottom + newValue,
+ x: x + newValue,
+ y: y + newValue,
+ };
+ });
+ });
+ };
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('basic SVG creation', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders an SVG of the right size', () => {
+ expect(findLinkSvg().exists()).toBe(true);
+ expect(findLinkSvg().attributes('width')).toBe(
+ `${defaultProps.containerMeasurements.width}px`,
+ );
+ expect(findLinkSvg().attributes('height')).toBe(
+ `${defaultProps.containerMeasurements.height}px`,
+ );
+ });
+ });
+
+ describe('no pipeline data', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the component', () => {
+ expect(findLinkSvg().exists()).toBe(true);
+ expect(findAllLinksPath()).toHaveLength(0);
+ });
+ });
+
+ describe('pipeline data with no needs', () => {
+ beforeEach(() => {
+ createComponent({ pipelineData: pipelineDataWithNoNeeds.stages });
+ });
+
+ it('renders no links', () => {
+ expect(findLinkSvg().exists()).toBe(true);
+ expect(findAllLinksPath()).toHaveLength(0);
+ });
+ });
+
+ describe('with one need', () => {
+ beforeEach(() => {
+ setFixtures(pipelineData);
+ createComponent({ pipelineData: pipelineData.stages });
+ });
+
+ it('renders one link', () => {
+ expect(findAllLinksPath()).toHaveLength(1);
+ });
+
+ it('path does not contain NaN values', () => {
+ expect(wrapper.html()).not.toContain('NaN');
+ });
+
+ it('matches snapshot and has expected path', () => {
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+ });
+
+ describe('with a parallel need', () => {
+ beforeEach(() => {
+ setFixtures(parallelNeedData);
+ createComponent({ pipelineData: parallelNeedData.stages });
+ });
+
+ it('renders only one link for all the same parallel jobs', () => {
+ expect(findAllLinksPath()).toHaveLength(1);
+ });
+
+ it('path does not contain NaN values', () => {
+ expect(wrapper.html()).not.toContain('NaN');
+ });
+
+ it('matches snapshot and has expected path', () => {
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+ });
+
+ describe('with a large number of needs', () => {
+ beforeEach(() => {
+ setFixtures(largePipelineData);
+ createComponent({ pipelineData: largePipelineData.stages });
+ });
+
+ it('renders the correct number of links', () => {
+ expect(findAllLinksPath()).toHaveLength(5);
+ });
+
+ it('path does not contain NaN values', () => {
+ expect(wrapper.html()).not.toContain('NaN');
+ });
+
+ it('matches snapshot and has expected path', () => {
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+ });
+
+ describe('interactions', () => {
+ beforeEach(() => {
+ setFixtures(largePipelineData);
+ createComponent({ pipelineData: largePipelineData.stages });
+ });
+
+ it('highlight needs on hover', async () => {
+ const firstLink = findAllLinksPath().at(0);
+
+ const defaultColorClass = 'gl-stroke-gray-200';
+ const hoverColorClass = 'gl-stroke-blue-400';
+
+ expect(firstLink.classes(defaultColorClass)).toBe(true);
+ expect(firstLink.classes(hoverColorClass)).toBe(false);
+
+ // Because there is a watcher, we need to set the props after the component
+ // has mounted.
+ await wrapper.setProps({ highlightedJob: 'test_1' });
+
+ expect(firstLink.classes(defaultColorClass)).toBe(false);
+ expect(firstLink.classes(hoverColorClass)).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/shared/links_layer_spec.js b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
index 9ef5233dbce..0ff8583fbff 100644
--- a/spec/frontend/pipelines/shared/links_layer_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
@@ -1,7 +1,7 @@
-import { mount, shallowMount } from '@vue/test-utils';
import { GlAlert, GlButton } from '@gitlab/ui';
-import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
+import { mount, shallowMount } from '@vue/test-utils';
import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
+import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import { generateResponse, mockPipelineResponse } from '../graph/mock_data';
describe('links layer component', () => {
diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js
index 03e385e3cc8..57d846c53c8 100644
--- a/spec/frontend/pipelines/header_component_spec.js
+++ b/spec/frontend/pipelines/header_component_spec.js
@@ -1,15 +1,15 @@
-import { shallowMount } from '@vue/test-utils';
import { GlModal, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import HeaderComponent from '~/pipelines/components/header_component.vue';
+import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
+import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql';
+import retryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
import {
mockCancelledPipelineHeader,
mockFailedPipelineHeader,
mockRunningPipelineHeader,
mockSuccessfulPipelineHeader,
} from './mock_data';
-import HeaderComponent from '~/pipelines/components/header_component.vue';
-import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql';
-import retryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
-import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
describe('Pipeline details header', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/legacy_header_component_spec.js b/spec/frontend/pipelines/legacy_header_component_spec.js
deleted file mode 100644
index fb7feb8898a..00000000000
--- a/spec/frontend/pipelines/legacy_header_component_spec.js
+++ /dev/null
@@ -1,116 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlModal } from '@gitlab/ui';
-import LegacyHeaderComponent from '~/pipelines/components/legacy_header_component.vue';
-import CiHeader from '~/vue_shared/components/header_ci_component.vue';
-import eventHub from '~/pipelines/event_hub';
-
-describe('Pipeline details header', () => {
- let wrapper;
- let glModalDirective;
-
- const threeWeeksAgo = new Date();
- threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
-
- const findDeleteModal = () => wrapper.find(GlModal);
-
- const defaultProps = {
- pipeline: {
- details: {
- status: {
- group: 'failed',
- icon: 'status_failed',
- label: 'failed',
- text: 'failed',
- details_path: 'path',
- },
- },
- id: 123,
- created_at: threeWeeksAgo.toISOString(),
- user: {
- web_url: 'path',
- name: 'Foo',
- username: 'foobar',
- email: 'foo@bar.com',
- avatar_url: 'link',
- },
- retry_path: 'retry',
- cancel_path: 'cancel',
- delete_path: 'delete',
- },
- isLoading: false,
- };
-
- const createComponent = (props = {}) => {
- glModalDirective = jest.fn();
-
- wrapper = shallowMount(LegacyHeaderComponent, {
- propsData: {
- ...props,
- },
- directives: {
- glModal: {
- bind(el, { value }) {
- glModalDirective(value);
- },
- },
- },
- });
- };
-
- beforeEach(() => {
- jest.spyOn(eventHub, '$emit');
-
- createComponent(defaultProps);
- });
-
- afterEach(() => {
- eventHub.$off();
-
- wrapper.destroy();
- wrapper = null;
- });
-
- it('should render provided pipeline info', () => {
- expect(wrapper.find(CiHeader).props()).toMatchObject({
- status: defaultProps.pipeline.details.status,
- itemId: defaultProps.pipeline.id,
- time: defaultProps.pipeline.created_at,
- user: defaultProps.pipeline.user,
- });
- });
-
- describe('action buttons', () => {
- it('should not trigger eventHub when nothing happens', () => {
- expect(eventHub.$emit).not.toHaveBeenCalled();
- });
-
- it('should call postAction when retry button action is clicked', () => {
- wrapper.find('[data-testid="retryButton"]').vm.$emit('click');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'retry');
- });
-
- it('should call postAction when cancel button action is clicked', () => {
- wrapper.find('[data-testid="cancelPipeline"]').vm.$emit('click');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'cancel');
- });
-
- it('does not show delete modal', () => {
- expect(findDeleteModal()).not.toBeVisible();
- });
-
- describe('when delete button action is clicked', () => {
- it('displays delete modal', () => {
- expect(findDeleteModal().props('modalId')).toBe(wrapper.vm.$options.DELETE_MODAL_ID);
- expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.$options.DELETE_MODAL_ID);
- });
-
- it('should call delete when modal is submitted', () => {
- findDeleteModal().vm.$emit('ok');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('headerDeleteAction', 'delete');
- });
- });
- });
-});
diff --git a/spec/frontend/pipelines/pipeline_graph/mock_data.js b/spec/frontend/pipelines/pipeline_graph/mock_data.js
index 7d1a7a79c7f..339aac9f349 100644
--- a/spec/frontend/pipelines/pipeline_graph/mock_data.js
+++ b/spec/frontend/pipelines/pipeline_graph/mock_data.js
@@ -1,5 +1,3 @@
-import { createUniqueLinkId } from '~/pipelines/components/graph_shared/drawing_utils';
-
export const yamlString = `stages:
- empty
- build
@@ -41,10 +39,28 @@ deploy_a:
script: echo hello
`;
-const jobId1 = createUniqueLinkId('build', 'build_1');
-const jobId2 = createUniqueLinkId('test', 'test_1');
-const jobId3 = createUniqueLinkId('test', 'test_2');
-const jobId4 = createUniqueLinkId('deploy', 'deploy_1');
+export const pipelineDataWithNoNeeds = {
+ stages: [
+ {
+ name: 'build',
+ groups: [
+ {
+ name: 'build_1',
+ jobs: [{ script: 'echo hello', stage: 'build' }],
+ },
+ ],
+ },
+ {
+ name: 'test',
+ groups: [
+ {
+ name: 'test_1',
+ jobs: [{ script: 'yarn test', stage: 'test' }],
+ },
+ ],
+ },
+ ],
+};
export const pipelineData = {
stages: [
@@ -54,7 +70,6 @@ export const pipelineData = {
{
name: 'build_1',
jobs: [{ script: 'echo hello', stage: 'build' }],
- id: jobId1,
},
],
},
@@ -64,12 +79,10 @@ export const pipelineData = {
{
name: 'test_1',
jobs: [{ script: 'yarn test', stage: 'test' }],
- id: jobId2,
},
{
name: 'test_2',
jobs: [{ script: 'yarn karma', stage: 'test' }],
- id: jobId3,
},
],
},
@@ -79,7 +92,86 @@ export const pipelineData = {
{
name: 'deploy_1',
jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['test_1'] }],
- id: jobId4,
+ },
+ ],
+ },
+ ],
+};
+
+export const parallelNeedData = {
+ stages: [
+ {
+ name: 'build',
+ groups: [
+ {
+ name: 'build_1',
+ parallel: 3,
+ jobs: [
+ { script: 'echo hello', stage: 'build', name: 'build_1 1/3' },
+ { script: 'echo hello', stage: 'build', name: 'build_1 2/3' },
+ { script: 'echo hello', stage: 'build', name: 'build_1 3/3' },
+ ],
+ },
+ ],
+ },
+ {
+ name: 'test',
+ groups: [
+ {
+ name: 'test_1',
+ jobs: [{ script: 'yarn test', stage: 'test', needs: ['build_1'] }],
+ },
+ ],
+ },
+ ],
+};
+
+export const largePipelineData = {
+ stages: [
+ {
+ name: 'build',
+ groups: [
+ {
+ name: 'build_1',
+ jobs: [{ script: 'echo hello', stage: 'build' }],
+ },
+ {
+ name: 'build_2',
+ jobs: [{ script: 'echo hello', stage: 'build' }],
+ },
+ {
+ name: 'build_3',
+ jobs: [{ script: 'echo hello', stage: 'build' }],
+ },
+ ],
+ },
+ {
+ name: 'test',
+ groups: [
+ {
+ name: 'test_1',
+ jobs: [{ script: 'yarn test', stage: 'test', needs: ['build_2'] }],
+ },
+ {
+ name: 'test_2',
+ jobs: [{ script: 'yarn karma', stage: 'test', needs: ['build_2'] }],
+ },
+ ],
+ },
+ {
+ name: 'deploy',
+ groups: [
+ {
+ name: 'deploy_1',
+ jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['test_1'] }],
+ },
+ {
+ name: 'deploy_2',
+ jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['build_3'] }],
+ },
+ {
+ name: 'deploy_3',
+ jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['test_2'] }],
},
],
},
@@ -94,9 +186,30 @@ export const singleStageData = {
{
name: 'build_1',
jobs: [{ script: 'echo hello', stage: 'build' }],
- id: jobId1,
},
],
},
],
};
+
+export const rootRect = {
+ bottom: 463,
+ height: 271,
+ left: 236,
+ right: 1252,
+ top: 192,
+ width: 1016,
+ x: 236,
+ y: 192,
+};
+
+export const jobRect = {
+ bottom: 312,
+ height: 24,
+ left: 308,
+ right: 428,
+ top: 288,
+ width: 120,
+ x: 308,
+ y: 288,
+};
diff --git a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
index b6b0a964383..718667fcc73 100644
--- a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
+++ b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
@@ -1,11 +1,11 @@
-import { shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
-import { pipelineData, singleStageData } from './mock_data';
+import { shallowMount } from '@vue/test-utils';
import { CI_CONFIG_STATUS_INVALID, CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants';
-import { DRAW_FAILURE, EMPTY_PIPELINE_DATA, INVALID_CI_CONFIG } from '~/pipelines/constants';
+import JobPill from '~/pipelines/components/pipeline_graph/job_pill.vue';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import StagePill from '~/pipelines/components/pipeline_graph/stage_pill.vue';
-import JobPill from '~/pipelines/components/pipeline_graph/job_pill.vue';
+import { DRAW_FAILURE, EMPTY_PIPELINE_DATA, INVALID_CI_CONFIG } from '~/pipelines/constants';
+import { pipelineData, singleStageData } from './mock_data';
describe('pipeline graph component', () => {
const defaultProps = { pipelineData };
diff --git a/spec/frontend/pipelines/pipeline_triggerer_spec.js b/spec/frontend/pipelines/pipeline_triggerer_spec.js
index ad8136890e6..467a97d95c7 100644
--- a/spec/frontend/pipelines/pipeline_triggerer_spec.js
+++ b/spec/frontend/pipelines/pipeline_triggerer_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import pipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
describe('Pipelines Triggerer', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js
index 47315bd42e6..44c9def99cc 100644
--- a/spec/frontend/pipelines/pipeline_url_spec.js
+++ b/spec/frontend/pipelines/pipeline_url_spec.js
@@ -1,6 +1,6 @@
+import { shallowMount } from '@vue/test-utils';
import $ from 'jquery';
import { trimText } from 'helpers/text_helper';
-import { shallowMount } from '@vue/test-utils';
import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue';
$.fn.popover = () => {};
@@ -17,6 +17,7 @@ describe('Pipeline Url Component', () => {
const findStuckTag = () => wrapper.find('[data-testid="pipeline-url-stuck"]');
const findDetachedTag = () => wrapper.find('[data-testid="pipeline-url-detached"]');
const findForkTag = () => wrapper.find('[data-testid="pipeline-url-fork"]');
+ const findTrainTag = () => wrapper.find('[data-testid="pipeline-url-train"]');
const defaultProps = {
pipeline: {
@@ -141,6 +142,7 @@ describe('Pipeline Url Component', () => {
expect(findScheduledTag().exists()).toBe(true);
expect(findScheduledTag().text()).toContain('Scheduled');
});
+
it('should render the fork badge when the pipeline was run in a fork', () => {
createComponent({
pipeline: {
@@ -152,4 +154,28 @@ describe('Pipeline Url Component', () => {
expect(findForkTag().exists()).toBe(true);
expect(findForkTag().text()).toBe('fork');
});
+
+ it('should render the train badge when the pipeline is a merge train pipeline', () => {
+ createComponent({
+ pipeline: {
+ flags: {
+ merge_train_pipeline: true,
+ },
+ },
+ });
+
+ expect(findTrainTag().text()).toContain('train');
+ });
+
+ it('should not render the train badge when the pipeline is not a merge train pipeline', () => {
+ createComponent({
+ pipeline: {
+ flags: {
+ merge_train_pipeline: false,
+ },
+ },
+ });
+
+ expect(findTrainTag().exists()).toBe(false);
+ });
});
diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js
index 69c1b7ce43d..1e6c9e50a7e 100644
--- a/spec/frontend/pipelines/pipelines_actions_spec.js
+++ b/spec/frontend/pipelines/pipelines_actions_spec.js
@@ -1,25 +1,29 @@
-import { shallowMount } from '@vue/test-utils';
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'spec/test_constants';
-import { GlButton } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
+import { TEST_HOST } from 'spec/test_constants';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import PipelinesActions from '~/pipelines/components/pipelines_list/pipelines_actions.vue';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+jest.mock('~/flash');
+
describe('Pipelines Actions dropdown', () => {
let wrapper;
let mock;
- const createComponent = (actions = []) => {
- wrapper = shallowMount(PipelinesActions, {
+ const createComponent = (props, mountFn = shallowMount) => {
+ wrapper = mountFn(PipelinesActions, {
propsData: {
- actions,
+ ...props,
},
});
};
- const findAllDropdownItems = () => wrapper.findAll(GlButton);
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findAllCountdowns = () => wrapper.findAll(GlCountdown);
beforeEach(() => {
@@ -47,7 +51,7 @@ describe('Pipelines Actions dropdown', () => {
];
beforeEach(() => {
- createComponent(mockActions);
+ createComponent({ actions: mockActions });
});
it('renders a dropdown with the provided actions', () => {
@@ -59,16 +63,33 @@ describe('Pipelines Actions dropdown', () => {
});
describe('on click', () => {
- it('makes a request and toggles the loading state', () => {
+ beforeEach(() => {
+ createComponent({ actions: mockActions }, mount);
+ });
+
+ it('makes a request and toggles the loading state', async () => {
mock.onPost(mockActions.path).reply(200);
- wrapper.find(GlButton).vm.$emit('click');
+ findAllDropdownItems().at(0).vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+ expect(findDropdown().props('loading')).toBe(true);
+
+ await waitForPromises();
+ expect(findDropdown().props('loading')).toBe(false);
+ });
+
+ it('makes a failed request and toggles the loading state', async () => {
+ mock.onPost(mockActions.path).reply(500);
- expect(wrapper.vm.isLoading).toBe(true);
+ findAllDropdownItems().at(0).vm.$emit('click');
- return waitForPromises().then(() => {
- expect(wrapper.vm.isLoading).toBe(false);
- });
+ await wrapper.vm.$nextTick();
+ expect(findDropdown().props('loading')).toBe(true);
+
+ await waitForPromises();
+ expect(findDropdown().props('loading')).toBe(false);
+ expect(createFlash).toHaveBeenCalledTimes(1);
});
});
});
@@ -89,10 +110,10 @@ describe('Pipelines Actions dropdown', () => {
beforeEach(() => {
jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
- createComponent([scheduledJobAction, expiredJobAction]);
+ createComponent({ actions: [scheduledJobAction, expiredJobAction] });
});
- it('makes post request after confirming', () => {
+ it('makes post request after confirming', async () => {
mock.onPost(scheduledJobAction.path).reply(200);
jest.spyOn(window, 'confirm').mockReturnValue(true);
@@ -100,19 +121,22 @@ describe('Pipelines Actions dropdown', () => {
expect(window.confirm).toHaveBeenCalled();
- return waitForPromises().then(() => {
- expect(mock.history.post.length).toBe(1);
- });
+ await waitForPromises();
+
+ expect(mock.history.post).toHaveLength(1);
});
- it('does not make post request if confirmation is cancelled', () => {
+ it('does not make post request if confirmation is cancelled', async () => {
mock.onPost(scheduledJobAction.path).reply(200);
jest.spyOn(window, 'confirm').mockReturnValue(false);
findAllDropdownItems().at(0).vm.$emit('click');
expect(window.confirm).toHaveBeenCalled();
- expect(mock.history.post.length).toBe(0);
+
+ await waitForPromises();
+
+ expect(mock.history.post).toHaveLength(0);
});
it('displays the remaining time in the dropdown', () => {
diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js
index 4f4c15fd4cc..f077833ae16 100644
--- a/spec/frontend/pipelines/pipelines_artifacts_spec.js
+++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
describe('Pipelines Artifacts dropdown', () => {
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index 5d82669b0b8..811303a5624 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -1,49 +1,50 @@
-import { nextTick } from 'vue';
+import { GlFilteredSearch, GlButton, GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { chunk } from 'lodash';
+import { nextTick } from 'vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { GlFilteredSearch, GlButton, GlLoadingIcon } from '@gitlab/ui';
import Api from '~/api';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
-import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
-
-import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
-import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue';
import BlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
-import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
-
+import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue';
+import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue';
+import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
+import { RAW_TEXT_WARNING } from '~/pipelines/constants';
import Store from '~/pipelines/stores/pipelines_store';
+import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
+
import { pipelineWithStages, stageReply, users, mockSearch, branches } from './mock_data';
-import { RAW_TEXT_WARNING } from '~/pipelines/constants';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
-describe('Pipelines', () => {
- const jsonFixtureName = 'pipelines/pipelines.json';
+const mockProjectPath = 'twitter/flight';
+const mockProjectId = '21';
+const mockPipelinesEndpoint = `/${mockProjectPath}/pipelines.json`;
+const mockPipelinesResponse = getJSONFixture('pipelines/pipelines.json');
+const mockPipelinesIds = mockPipelinesResponse.pipelines.map(({ id }) => id);
- preloadFixtures(jsonFixtureName);
-
- let pipelines;
+describe('Pipelines', () => {
let wrapper;
let mock;
+ let origWindowLocation;
const paths = {
- endpoint: 'twitter/flight/pipelines.json',
autoDevopsHelpPath: '/help/topics/autodevops/index.md',
helpPagePath: '/help/ci/quick_start/README',
emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
ciLintPath: '/ci/lint',
- resetCachePath: '/twitter/flight/settings/ci_cd/reset_cache',
- newPipelinePath: '/twitter/flight/pipelines/new',
+ resetCachePath: `${mockProjectPath}/settings/ci_cd/reset_cache`,
+ newPipelinePath: `${mockProjectPath}/pipelines/new`,
};
const noPermissions = {
- endpoint: 'twitter/flight/pipelines.json',
autoDevopsHelpPath: '/help/topics/autodevops/index.md',
helpPagePath: '/help/ci/quick_start/README',
emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
@@ -57,101 +58,140 @@ describe('Pipelines', () => {
...paths,
};
- const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
- const findByTestId = (id) => wrapper.find(`[data-testid="${id}"]`);
- const findNavigationTabs = () => wrapper.find(NavigationTabs);
- const findNavigationControls = () => wrapper.find(NavigationControls);
- const findTab = (tab) => findByTestId(`pipelines-tab-${tab}`);
-
- const findRunPipelineButton = () => findByTestId('run-pipeline-button');
- const findCiLintButton = () => findByTestId('ci-lint-button');
- const findCleanCacheButton = () => findByTestId('clear-cache-button');
-
- const findEmptyState = () => wrapper.find(EmptyState);
- const findBlankState = () => wrapper.find(BlankState);
- const findStagesDropdown = () => wrapper.find('.js-builds-dropdown-button');
-
- const findTablePagination = () => wrapper.find(TablePagination);
+ const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
+ const findNavigationTabs = () => wrapper.findComponent(NavigationTabs);
+ const findNavigationControls = () => wrapper.findComponent(NavigationControls);
+ const findPipelinesTable = () => wrapper.findComponent(PipelinesTableComponent);
+ const findEmptyState = () => wrapper.findComponent(EmptyState);
+ const findBlankState = () => wrapper.findComponent(BlankState);
+ const findTablePagination = () => wrapper.findComponent(TablePagination);
+
+ const findTab = (tab) => wrapper.findByTestId(`pipelines-tab-${tab}`);
+ const findRunPipelineButton = () => wrapper.findByTestId('run-pipeline-button');
+ const findCiLintButton = () => wrapper.findByTestId('ci-lint-button');
+ const findCleanCacheButton = () => wrapper.findByTestId('clear-cache-button');
+ const findStagesDropdown = () => wrapper.findByTestId('mini-pipeline-graph-dropdown-toggle');
+ const findPipelineUrlLinks = () => wrapper.findAll('[data-testid="pipeline-url-link"]');
const createComponent = (props = defaultProps) => {
- wrapper = mount(PipelinesComponent, {
- propsData: {
- store: new Store(),
- projectId: '21',
- params: {},
- ...props,
- },
- });
+ wrapper = extendedWrapper(
+ mount(PipelinesComponent, {
+ propsData: {
+ store: new Store(),
+ projectId: mockProjectId,
+ endpoint: mockPipelinesEndpoint,
+ params: {},
+ ...props,
+ },
+ }),
+ );
};
- beforeEach(() => {
+ beforeAll(() => {
+ origWindowLocation = window.location;
delete window.location;
+ window.location = { search: '' };
+ });
+
+ afterAll(() => {
+ window.location = origWindowLocation;
});
beforeEach(() => {
- window.location = { search: '' };
mock = new MockAdapter(axios);
- pipelines = getJSONFixture(jsonFixtureName);
+ jest.spyOn(window.history, 'pushState');
jest.spyOn(Api, 'projectUsers').mockResolvedValue(users);
jest.spyOn(Api, 'branches').mockResolvedValue({ data: branches });
});
afterEach(() => {
wrapper.destroy();
- mock.restore();
+ mock.reset();
+ window.history.pushState.mockReset();
});
- describe('With permission', () => {
- describe('With pipelines in main tab', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
- createComponent();
- return waitForPromises();
- });
+ describe('when pipelines are not yet loaded', () => {
+ beforeEach(async () => {
+ createComponent();
+ await nextTick();
+ });
- it('renders tabs', () => {
- expect(findTab('all').text()).toContain('All');
- });
+ it('shows loading state when the app is loading', () => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
- it('renders Run Pipeline link', () => {
- expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
+ it('does not display tabs when the first request has not yet been made', () => {
+ expect(findNavigationTabs().exists()).toBe(false);
+ });
+
+ it('does not display buttons', () => {
+ expect(findNavigationControls().exists()).toBe(false);
+ });
+ });
+
+ describe('when there are pipelines in the project', () => {
+ beforeEach(() => {
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } })
+ .reply(200, mockPipelinesResponse);
+ });
+
+ describe('when user has no permissions', () => {
+ beforeEach(async () => {
+ createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions });
+ await waitForPromises();
});
- it('renders CI Lint link', () => {
- expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
+ it('renders "All" tab with count different from "0"', () => {
+ expect(findTab('all').text()).toMatchInterpolatedText('All 3');
});
- it('renders Clear Runner Cache button', () => {
- expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
+ it('does not render buttons', () => {
+ expect(findNavigationControls().exists()).toBe(false);
+
+ expect(findRunPipelineButton().exists()).toBe(false);
+ expect(findCiLintButton().exists()).toBe(false);
+ expect(findCleanCacheButton().exists()).toBe(false);
});
- it('renders pipelines table', () => {
- expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength(
- pipelines.pipelines.length + 1,
- );
+ it('renders pipelines in a table', () => {
+ expect(findPipelinesTable().exists()).toBe(true);
+
+ expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockPipelinesIds[0]}`);
+ expect(findPipelineUrlLinks().at(1).text()).toBe(`#${mockPipelinesIds[1]}`);
+ expect(findPipelineUrlLinks().at(2).text()).toBe(`#${mockPipelinesIds[2]}`);
});
});
- describe('Without pipelines on main tab with CI', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
- });
-
+ describe('when user has permissions', () => {
+ beforeEach(async () => {
createComponent();
+ await waitForPromises();
+ });
- return waitForPromises();
+ it('should set up navigation tabs', () => {
+ expect(findNavigationTabs().props('tabs')).toEqual([
+ { name: 'All', scope: 'all', count: '3', isActive: true },
+ { name: 'Finished', scope: 'finished', count: undefined, isActive: false },
+ { name: 'Branches', scope: 'branches', isActive: false },
+ { name: 'Tags', scope: 'tags', isActive: false },
+ ]);
});
- it('renders tabs', () => {
- expect(findTab('all').text()).toContain('All');
+ it('renders "All" tab with count different from "0"', () => {
+ expect(findTab('all').text()).toMatchInterpolatedText('All 3');
+ });
+
+ it('should render other navigation tabs', () => {
+ expect(findTab('finished').text()).toBe('Finished');
+ expect(findTab('branches').text()).toBe('Branches');
+ expect(findTab('tags').text()).toBe('Tags');
+ });
+
+ it('shows navigation controls', () => {
+ expect(findNavigationControls().exists()).toBe(true);
});
it('renders Run Pipeline link', () => {
@@ -166,549 +206,513 @@ describe('Pipelines', () => {
expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
});
- it('renders tab empty state', () => {
- expect(findBlankState().text()).toBe('There are currently no pipelines.');
- });
-
- it('renders tab empty state finished scope', () => {
- wrapper.vm.scope = 'finished';
+ it('renders pipelines in a table', () => {
+ expect(findPipelinesTable().exists()).toBe(true);
- return nextTick().then(() => {
- expect(findBlankState().text()).toBe('There are currently no finished pipelines.');
- });
+ expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockPipelinesIds[0]}`);
+ expect(findPipelineUrlLinks().at(1).text()).toBe(`#${mockPipelinesIds[1]}`);
+ expect(findPipelineUrlLinks().at(2).text()).toBe(`#${mockPipelinesIds[2]}`);
});
- });
-
- describe('Without pipelines nor CI', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
- });
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+ describe('when user goes to a tab', () => {
+ const goToTab = (tab) => {
+ findNavigationTabs().vm.$emit('onChangeTab', tab);
+ };
- return waitForPromises();
- });
+ describe('when the scope in the tab has pipelines', () => {
+ const mockFinishedPipeline = mockPipelinesResponse.pipelines[0];
- it('renders empty state', () => {
- expect(findEmptyState().find('h4').text()).toBe('Build with confidence');
- expect(findEmptyState().find(GlButton).attributes('href')).toBe(paths.helpPagePath);
- });
+ beforeEach(async () => {
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } })
+ .reply(200, {
+ pipelines: [mockFinishedPipeline],
+ count: mockPipelinesResponse.count,
+ });
- it('does not render tabs nor buttons', () => {
- expect(findTab('all').exists()).toBe(false);
- expect(findRunPipelineButton().exists()).toBeFalsy();
- expect(findCiLintButton().exists()).toBeFalsy();
- expect(findCleanCacheButton().exists()).toBeFalsy();
- });
- });
+ goToTab('finished');
- describe('When API returns error', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(500, {});
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+ await waitForPromises();
+ });
- return waitForPromises();
- });
+ it('should filter pipelines', async () => {
+ expect(findPipelinesTable().exists()).toBe(true);
- it('renders tabs', () => {
- expect(findTab('all').text()).toContain('All');
- });
+ expect(findPipelineUrlLinks()).toHaveLength(1);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFinishedPipeline.id}`);
+ });
- it('renders buttons', () => {
- expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
+ it('should update browser bar', () => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?scope=finished&page=1`,
+ );
+ });
+ });
- expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
- expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
- });
+ describe('when the scope in the tab is empty', () => {
+ beforeEach(async () => {
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'branches', page: '1' } })
+ .reply(200, {
+ pipelines: [],
+ count: mockPipelinesResponse.count,
+ });
- it('renders error state', () => {
- expect(findBlankState().text()).toContain('There was an error fetching the pipelines.');
- });
- });
- });
+ goToTab('branches');
- describe('Without permission', () => {
- describe('With pipelines in main tab', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+ await waitForPromises();
+ });
- createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions });
+ it('should filter pipelines', async () => {
+ expect(findBlankState().text()).toBe('There are currently no pipelines.');
+ });
- return waitForPromises();
+ it('should update browser bar', () => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?scope=branches&page=1`,
+ );
+ });
+ });
});
- it('renders tabs', () => {
- expect(findTab('all').text()).toContain('All');
- });
+ describe('when user triggers a filtered search', () => {
+ const mockFilteredPipeline = mockPipelinesResponse.pipelines[1];
- it('does not render buttons', () => {
- expect(findRunPipelineButton().exists()).toBeFalsy();
- expect(findCiLintButton().exists()).toBeFalsy();
- expect(findCleanCacheButton().exists()).toBeFalsy();
- });
+ let expectedParams;
- it('renders pipelines table', () => {
- expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength(
- pipelines.pipelines.length + 1,
- );
- });
- });
+ beforeEach(async () => {
+ expectedParams = {
+ page: '1',
+ scope: 'all',
+ username: 'root',
+ ref: 'master',
+ status: 'pending',
+ };
+
+ mock
+ .onGet(mockPipelinesEndpoint, {
+ params: expectedParams,
+ })
+ .replyOnce(200, {
+ pipelines: [mockFilteredPipeline],
+ count: mockPipelinesResponse.count,
+ });
- describe('Without pipelines on main tab with CI', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
+ findFilteredSearch().vm.$emit('submit', mockSearch);
+
+ await waitForPromises();
});
- createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions });
+ it('requests data with query params on filter submit', async () => {
+ expect(mock.history.get[1].params).toEqual(expectedParams);
+ });
- return waitForPromises();
- });
+ it('renders filtered pipelines', async () => {
+ expect(findPipelineUrlLinks()).toHaveLength(1);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFilteredPipeline.id}`);
+ });
- it('renders tabs', () => {
- expect(findTab('all').text()).toContain('All');
+ it('should update browser bar', () => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?page=1&scope=all&username=root&ref=master&status=pending`,
+ );
+ });
});
- it('does not render buttons', () => {
- expect(findRunPipelineButton().exists()).toBeFalsy();
- expect(findCiLintButton().exists()).toBeFalsy();
- expect(findCleanCacheButton().exists()).toBeFalsy();
- });
+ describe('when user triggers a filtered search with raw text', () => {
+ beforeEach(async () => {
+ findFilteredSearch().vm.$emit('submit', ['rawText']);
- it('renders tab empty state', () => {
- expect(wrapper.find('.empty-state h4').text()).toBe('There are currently no pipelines.');
- });
- });
+ await waitForPromises();
+ });
- describe('Without pipelines nor CI', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
+ it('requests data with query params on filter submit', async () => {
+ expect(mock.history.get[1].params).toEqual({ page: '1', scope: 'all' });
});
- createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions });
+ it('displays a warning message if raw text search is used', () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(RAW_TEXT_WARNING, 'warning');
+ });
- return waitForPromises();
+ it('should update browser bar', () => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?page=1&scope=all`,
+ );
+ });
});
+ });
+ });
- it('renders empty state without button to set CI', () => {
- expect(findEmptyState().text()).toBe(
- 'This project is not currently set up to run pipelines.',
- );
+ describe('when there are multiple pages of pipelines', () => {
+ const mockPageSize = 2;
+ const mockPageHeaders = ({ page = 1 } = {}) => {
+ return {
+ 'X-PER-PAGE': `${mockPageSize}`,
+ 'X-PREV-PAGE': `${page - 1}`,
+ 'X-PAGE': `${page}`,
+ 'X-NEXT-PAGE': `${page + 1}`,
+ };
+ };
+ const [firstPage, secondPage] = chunk(mockPipelinesResponse.pipelines, mockPageSize);
+
+ const goToPage = (page) => {
+ findTablePagination().find(GlPagination).vm.$emit('input', page);
+ };
+
+ beforeEach(async () => {
+ mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }).reply(
+ 200,
+ {
+ pipelines: firstPage,
+ count: mockPipelinesResponse.count,
+ },
+ mockPageHeaders({ page: 1 }),
+ );
+ mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '2' } }).reply(
+ 200,
+ {
+ pipelines: secondPage,
+ count: mockPipelinesResponse.count,
+ },
+ mockPageHeaders({ page: 2 }),
+ );
- expect(findEmptyState().find(GlButton).exists()).toBeFalsy();
- });
+ createComponent();
- it('does not render tabs or buttons', () => {
- expect(findTab('all').exists()).toBe(false);
- expect(findRunPipelineButton().exists()).toBeFalsy();
- expect(findCiLintButton().exists()).toBeFalsy();
- expect(findCleanCacheButton().exists()).toBeFalsy();
- });
+ await waitForPromises();
});
- describe('When API returns error', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(500, {});
-
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...noPermissions });
+ it('shows the first page of pipelines', () => {
+ expect(findPipelineUrlLinks()).toHaveLength(firstPage.length);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${firstPage[0].id}`);
+ expect(findPipelineUrlLinks().at(1).text()).toBe(`#${firstPage[1].id}`);
+ });
- return waitForPromises();
- });
+ it('should not update browser bar', () => {
+ expect(window.history.pushState).not.toHaveBeenCalled();
+ });
- it('renders tabs', () => {
- expect(findTab('all').text()).toContain('All');
+ describe('when user goes to next page', () => {
+ beforeEach(async () => {
+ goToPage(2);
+ await waitForPromises();
});
- it('does not renders buttons', () => {
- expect(findRunPipelineButton().exists()).toBeFalsy();
- expect(findCiLintButton().exists()).toBeFalsy();
- expect(findCleanCacheButton().exists()).toBeFalsy();
+ it('should update page and keep scope the same scope', () => {
+ expect(findPipelineUrlLinks()).toHaveLength(secondPage.length);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${secondPage[0].id}`);
});
- it('renders error state', () => {
- expect(wrapper.find('.empty-state').text()).toContain(
- 'There was an error fetching the pipelines.',
+ it('should update browser bar', () => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?page=2&scope=all`,
);
});
});
});
- describe('successful request', () => {
- describe('with pipelines', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+ describe('when pipelines can be polled', () => {
+ beforeEach(() => {
+ const emptyResponse = {
+ pipelines: [],
+ count: { all: '0' },
+ };
+ // Mock no pipelines in the first attempt
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } })
+ .replyOnce(200, emptyResponse, {
+ 'POLL-INTERVAL': 100,
+ });
+ // Mock pipelines in the next attempt
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } })
+ .reply(200, mockPipelinesResponse, {
+ 'POLL-INTERVAL': 100,
+ });
+ });
+
+ describe('data is loaded for the first time', () => {
+ beforeEach(async () => {
createComponent();
- return waitForPromises();
+ await waitForPromises();
});
- it('should render table', () => {
- expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength(
- pipelines.pipelines.length + 1,
- );
+ it('shows tabs', () => {
+ expect(findNavigationTabs().exists()).toBe(true);
});
- it('should set up navigation tabs', () => {
- expect(findNavigationTabs().props('tabs')).toEqual([
- { name: 'All', scope: 'all', count: '3', isActive: true },
- { name: 'Finished', scope: 'finished', count: undefined, isActive: false },
- { name: 'Branches', scope: 'branches', isActive: false },
- { name: 'Tags', scope: 'tags', isActive: false },
- ]);
+ it('should update page and keep scope the same scope', () => {
+ expect(findPipelineUrlLinks()).toHaveLength(0);
});
- it('should render navigation tabs', () => {
- expect(findTab('all').html()).toContain('All');
- expect(findTab('finished').text()).toContain('Finished');
- expect(findTab('branches').text()).toContain('Branches');
- expect(findTab('tags').text()).toContain('Tags');
- });
-
- it('should make an API request when using tabs', () => {
- createComponent({ hasGitlabCi: true, canCreatePipeline: true, ...paths });
- jest.spyOn(wrapper.vm.service, 'getPipelines');
-
- return waitForPromises().then(() => {
- findTab('finished').trigger('click');
-
- expect(wrapper.vm.service.getPipelines).toHaveBeenCalledWith({
- scope: 'finished',
- page: '1',
- });
+ describe('data is loaded for a second time', () => {
+ beforeEach(async () => {
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
});
- });
-
- describe('with pagination', () => {
- it('should make an API request when using pagination', () => {
- createComponent({ hasGitlabCi: true, canCreatePipeline: true, ...paths });
- jest.spyOn(wrapper.vm.service, 'getPipelines');
- return waitForPromises()
- .then(() => {
- // Mock pagination
- wrapper.vm.store.state.pageInfo = {
- page: 1,
- total: 10,
- perPage: 2,
- nextPage: 2,
- totalPages: 5,
- };
+ it('shows tabs', () => {
+ expect(findNavigationTabs().exists()).toBe(true);
+ });
- return nextTick();
- })
- .then(() => {
- wrapper.find('.next-page-item').trigger('click');
- expect(wrapper.vm.service.getPipelines).toHaveBeenCalledWith({
- scope: 'all',
- page: '2',
- });
- });
+ it('is loading after a time', async () => {
+ expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockPipelinesIds[0]}`);
+ expect(findPipelineUrlLinks().at(1).text()).toBe(`#${mockPipelinesIds[1]}`);
+ expect(findPipelineUrlLinks().at(2).text()).toBe(`#${mockPipelinesIds[2]}`);
});
});
});
});
- describe('User Interaction', () => {
- let updateContentMock;
-
+ describe('when no pipelines exist', () => {
beforeEach(() => {
- jest.spyOn(window.history, 'pushState').mockImplementation(() => null);
- });
-
- beforeEach(() => {
- mock.onGet(paths.endpoint).reply(200, pipelines);
- createComponent();
-
- updateContentMock = jest.spyOn(wrapper.vm, 'updateContent');
-
- return waitForPromises();
+ mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }).reply(200, {
+ pipelines: [],
+ count: { all: '0' },
+ });
});
- describe('when user changes tabs', () => {
- it('should set page to 1', () => {
- findNavigationTabs().vm.$emit('onChangeTab', 'running');
+ describe('when CI is enabled and user has permissions', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
- expect(updateContentMock).toHaveBeenCalledWith({ scope: 'running', page: '1' });
+ it('renders tab with count of "0"', () => {
+ expect(findNavigationTabs().exists()).toBe(true);
+ expect(findTab('all').text()).toMatchInterpolatedText('All 0');
});
- });
- describe('when user changes page', () => {
- it('should update page and keep scope', () => {
- findTablePagination().vm.change(4);
+ it('renders Run Pipeline link', () => {
+ expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
+ });
- expect(updateContentMock).toHaveBeenCalledWith({ scope: wrapper.vm.scope, page: '4' });
+ it('renders CI Lint link', () => {
+ expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
});
- });
- describe('updates results when a staged is clicked', () => {
- beforeEach(() => {
- const copyPipeline = { ...pipelineWithStages };
- copyPipeline.id += 1;
- mock
- .onGet('twitter/flight/pipelines.json')
- .reply(
- 200,
- {
- pipelines: [pipelineWithStages],
- count: {
- all: 1,
- finished: 1,
- pending: 0,
- running: 0,
- },
- },
- {
- 'POLL-INTERVAL': 100,
- },
- )
- .onGet(pipelineWithStages.details.stages[0].dropdown_path)
- .reply(200, stageReply);
+ it('renders Clear Runner Cache button', () => {
+ expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
+ });
- createComponent();
+ it('renders empty state', () => {
+ expect(findBlankState().text()).toBe('There are currently no pipelines.');
});
- describe('when a request is being made', () => {
- it('stops polling, cancels the request, & restarts polling', () => {
- const stopMock = jest.spyOn(wrapper.vm.poll, 'stop');
- const restartMock = jest.spyOn(wrapper.vm.poll, 'restart');
- const cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel');
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
-
- return waitForPromises()
- .then(() => {
- wrapper.vm.isMakingRequest = true;
- findStagesDropdown().trigger('click');
- })
- .then(() => {
- expect(cancelMock).toHaveBeenCalled();
- expect(stopMock).toHaveBeenCalled();
- expect(restartMock).toHaveBeenCalled();
- });
+ it('renders tab empty state finished scope', async () => {
+ mock.onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } }).reply(200, {
+ pipelines: [],
+ count: { all: '0' },
});
- });
- describe('when no request is being made', () => {
- it('stops polling & restarts polling', () => {
- const stopMock = jest.spyOn(wrapper.vm.poll, 'stop');
- const restartMock = jest.spyOn(wrapper.vm.poll, 'restart');
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+ findNavigationTabs().vm.$emit('onChangeTab', 'finished');
- return waitForPromises()
- .then(() => {
- findStagesDropdown().trigger('click');
- expect(stopMock).toHaveBeenCalled();
- })
- .then(() => {
- expect(restartMock).toHaveBeenCalled();
- });
- });
- });
- });
- });
+ await waitForPromises();
- describe('Rendered content', () => {
- beforeEach(() => {
- createComponent();
+ expect(findBlankState().text()).toBe('There are currently no finished pipelines.');
+ });
});
- describe('displays different content', () => {
- it('shows loading state when the app is loading', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ describe('when CI is not enabled and user has permissions', () => {
+ beforeEach(async () => {
+ createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+ await waitForPromises();
});
- it('shows error state when app has error', () => {
- wrapper.vm.hasError = true;
- wrapper.vm.isLoading = false;
-
- return nextTick().then(() => {
- expect(findBlankState().props('message')).toBe(
- 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
- );
- });
+ it('renders empty state', () => {
+ expect(findEmptyState().find('[data-testid="header-text"]').text()).toBe(
+ 'Build with confidence',
+ );
+ expect(findEmptyState().find('[data-testid="info-text"]').text()).toContain(
+ 'GitLab CI/CD can automatically build, test, and deploy your code.',
+ );
+ expect(findEmptyState().find(GlButton).text()).toBe('Get started with CI/CD');
+ expect(findEmptyState().find(GlButton).attributes('href')).toBe(paths.helpPagePath);
});
- it('shows table list when app has pipelines', () => {
- wrapper.vm.isLoading = false;
- wrapper.vm.hasError = false;
- wrapper.vm.state.pipelines = pipelines.pipelines;
-
- return nextTick().then(() => {
- expect(wrapper.find(PipelinesTableComponent).exists()).toBe(true);
- });
+ it('does not render tabs nor buttons', () => {
+ expect(findNavigationTabs().exists()).toBe(false);
+ expect(findTab('all').exists()).toBe(false);
+ expect(findRunPipelineButton().exists()).toBe(false);
+ expect(findCiLintButton().exists()).toBe(false);
+ expect(findCleanCacheButton().exists()).toBe(false);
});
+ });
- it('shows empty tab when app does not have pipelines but project has pipelines', () => {
- wrapper.vm.state.count.all = 10;
- wrapper.vm.isLoading = false;
-
- return nextTick().then(() => {
- expect(findBlankState().exists()).toBe(true);
- expect(findBlankState().props('message')).toBe('There are currently no pipelines.');
- });
+ describe('when CI is not enabled and user has no permissions', () => {
+ beforeEach(async () => {
+ createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions });
+ await waitForPromises();
});
- it('shows empty tab when project has CI', () => {
- wrapper.vm.isLoading = false;
+ it('renders empty state without button to set CI', () => {
+ expect(findEmptyState().text()).toBe(
+ 'This project is not currently set up to run pipelines.',
+ );
- return nextTick().then(() => {
- expect(findBlankState().exists()).toBe(true);
- expect(findBlankState().props('message')).toBe('There are currently no pipelines.');
- });
+ expect(findEmptyState().find(GlButton).exists()).toBe(false);
});
- it('shows empty state when project does not have pipelines nor CI', () => {
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
-
- wrapper.vm.isLoading = false;
-
- return nextTick().then(() => {
- expect(wrapper.find(EmptyState).exists()).toBe(true);
- });
+ it('does not render tabs or buttons', () => {
+ expect(findTab('all').exists()).toBe(false);
+ expect(findRunPipelineButton().exists()).toBe(false);
+ expect(findCiLintButton().exists()).toBe(false);
+ expect(findCleanCacheButton().exists()).toBe(false);
});
});
- describe('displays tabs', () => {
- it('returns true when state is loading & has already made the first request', () => {
- wrapper.vm.isLoading = true;
- wrapper.vm.hasMadeRequest = true;
+ describe('when CI is enabled and user has no permissions', () => {
+ beforeEach(() => {
+ createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions });
- return nextTick().then(() => {
- expect(findNavigationTabs().exists()).toBe(true);
- });
+ return waitForPromises();
});
- it('returns true when state is tableList & has already made the first request', () => {
- wrapper.vm.isLoading = false;
- wrapper.vm.state.pipelines = pipelines.pipelines;
- wrapper.vm.hasMadeRequest = true;
-
- return nextTick().then(() => {
- expect(findNavigationTabs().exists()).toBe(true);
- });
+ it('renders tab with count of "0"', () => {
+ expect(findTab('all').text()).toMatchInterpolatedText('All 0');
});
- it('returns true when state is error & has already made the first request', () => {
- wrapper.vm.isLoading = false;
- wrapper.vm.hasError = true;
- wrapper.vm.hasMadeRequest = true;
+ it('does not render buttons', () => {
+ expect(findRunPipelineButton().exists()).toBe(false);
+ expect(findCiLintButton().exists()).toBe(false);
+ expect(findCleanCacheButton().exists()).toBe(false);
+ });
- return nextTick().then(() => {
- expect(findNavigationTabs().exists()).toBe(true);
- });
+ it('renders empty state', () => {
+ expect(findBlankState().text()).toBe('There are currently no pipelines.');
});
+ });
+ });
- it('returns true when state is empty tab & has already made the first request', () => {
- wrapper.vm.isLoading = false;
- wrapper.vm.state.count.all = 10;
- wrapper.vm.hasMadeRequest = true;
+ describe('when a pipeline with stages exists', () => {
+ describe('updates results when a staged is clicked', () => {
+ let stopMock;
+ let restartMock;
+ let cancelMock;
- return nextTick().then(() => {
- expect(findNavigationTabs().exists()).toBe(true);
- });
- });
+ beforeEach(() => {
+ mock.onGet(mockPipelinesEndpoint, { scope: 'all', page: '1' }).reply(
+ 200,
+ {
+ pipelines: [pipelineWithStages],
+ count: { all: '1' },
+ },
+ {
+ 'POLL-INTERVAL': 100,
+ },
+ );
+ mock.onGet(pipelineWithStages.details.stages[0].dropdown_path).reply(200, stageReply);
- it('returns false when has not made first request', () => {
- wrapper.vm.hasMadeRequest = false;
+ createComponent();
- return nextTick().then(() => {
- expect(findNavigationTabs().exists()).toBe(false);
- });
+ stopMock = jest.spyOn(wrapper.vm.poll, 'stop');
+ restartMock = jest.spyOn(wrapper.vm.poll, 'restart');
+ cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel');
});
- it('returns false when state is empty state', () => {
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
-
- wrapper.vm.isLoading = false;
- wrapper.vm.hasMadeRequest = true;
+ describe('when a request is being made', () => {
+ beforeEach(async () => {
+ mock.onGet(mockPipelinesEndpoint).reply(200, mockPipelinesResponse);
- return nextTick().then(() => {
- expect(findNavigationTabs().exists()).toBe(false);
+ await waitForPromises();
});
- });
- });
- describe('displays buttons', () => {
- it('returns true when it has paths & has made the first request', () => {
- wrapper.vm.hasMadeRequest = true;
+ it('stops polling, cancels the request, & restarts polling', async () => {
+ // Mock init a polling cycle
+ wrapper.vm.poll.options.notificationCallback(true);
+
+ findStagesDropdown().trigger('click');
- return nextTick().then(() => {
- expect(findNavigationControls().exists()).toBe(true);
+ await waitForPromises();
+
+ expect(cancelMock).toHaveBeenCalled();
+ expect(stopMock).toHaveBeenCalled();
+ expect(restartMock).toHaveBeenCalled();
});
- });
- it('returns false when it has not made the first request', () => {
- wrapper.vm.hasMadeRequest = false;
+ it('stops polling & restarts polling', async () => {
+ findStagesDropdown().trigger('click');
- return nextTick().then(() => {
- expect(findNavigationControls().exists()).toBe(false);
+ expect(cancelMock).not.toHaveBeenCalled();
+ expect(stopMock).toHaveBeenCalled();
+ expect(restartMock).toHaveBeenCalled();
});
});
});
});
- describe('Pipeline filters', () => {
- let updateContentMock;
-
- beforeEach(() => {
- mock.onGet(paths.endpoint).reply(200, pipelines);
- createComponent();
+ describe('when pipelines cannot be loaded', () => {
+ beforeEach(async () => {
+ mock.onGet(mockPipelinesEndpoint).reply(500, {});
+ });
- updateContentMock = jest.spyOn(wrapper.vm, 'updateContent');
+ describe('when user has no permissions', () => {
+ beforeEach(async () => {
+ createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...noPermissions });
- return waitForPromises();
- });
+ await waitForPromises();
+ });
- it('updates request data and query params on filter submit', async () => {
- const expectedQueryParams = {
- page: '1',
- scope: 'all',
- username: 'root',
- ref: 'master',
- status: 'pending',
- };
+ it('renders tabs', () => {
+ expect(findNavigationTabs().exists()).toBe(true);
+ expect(findTab('all').text()).toBe('All');
+ });
- findFilteredSearch().vm.$emit('submit', mockSearch);
- await nextTick();
+ it('does not render buttons', () => {
+ expect(findRunPipelineButton().exists()).toBe(false);
+ expect(findCiLintButton().exists()).toBe(false);
+ expect(findCleanCacheButton().exists()).toBe(false);
+ });
- expect(wrapper.vm.requestData).toEqual(expectedQueryParams);
- expect(updateContentMock).toHaveBeenCalledWith(expectedQueryParams);
+ it('shows error state', () => {
+ expect(findBlankState().text()).toBe(
+ 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
+ );
+ });
});
- it('does not add query params if raw text search is used', async () => {
- const expectedQueryParams = { page: '1', scope: 'all' };
+ describe('when user has permissions', () => {
+ beforeEach(async () => {
+ createComponent();
- findFilteredSearch().vm.$emit('submit', ['rawText']);
- await nextTick();
+ await waitForPromises();
+ });
- expect(wrapper.vm.requestData).toEqual(expectedQueryParams);
- expect(updateContentMock).toHaveBeenCalledWith(expectedQueryParams);
- });
+ it('renders tabs', () => {
+ expect(findTab('all').text()).toBe('All');
+ });
+
+ it('renders buttons', () => {
+ expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
- it('displays a warning message if raw text search is used', () => {
- findFilteredSearch().vm.$emit('submit', ['rawText']);
+ expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
+ expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
+ });
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith(RAW_TEXT_WARNING, 'warning');
+ it('shows error state', () => {
+ expect(findBlankState().text()).toBe(
+ 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
+ );
+ });
});
});
});
diff --git a/spec/frontend/pipelines/pipelines_table_row_spec.js b/spec/frontend/pipelines/pipelines_table_row_spec.js
index 9cdd24b2ab5..660651547fc 100644
--- a/spec/frontend/pipelines/pipelines_table_row_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_row_spec.js
@@ -155,7 +155,9 @@ describe('Pipelines Table Row', () => {
it('should render an icon for each stage', () => {
expect(
- wrapper.findAll('.table-section:nth-child(4) .js-builds-dropdown-button').length,
+ wrapper.findAll(
+ '.table-section:nth-child(4) [data-testid="mini-pipeline-graph-dropdown-toggle"]',
+ ).length,
).toEqual(pipeline.details.stages.length);
});
});
diff --git a/spec/frontend/pipelines/stage_spec.js b/spec/frontend/pipelines/stage_spec.js
index e4782a1dab1..87b43558252 100644
--- a/spec/frontend/pipelines/stage_spec.js
+++ b/spec/frontend/pipelines/stage_spec.js
@@ -1,6 +1,8 @@
import 'bootstrap/js/dist/dropdown';
+import { GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import StageComponent from '~/pipelines/components/pipelines_list/stage.vue';
import eventHub from '~/pipelines/event_hub';
@@ -9,6 +11,7 @@ import { stageReply } from './mock_data';
describe('Pipelines stage component', () => {
let wrapper;
let mock;
+ let glFeatures;
const defaultProps = {
stage: {
@@ -22,8 +25,6 @@ describe('Pipelines stage component', () => {
updateDropdown: false,
};
- const isDropdownOpen = () => wrapper.classes('show');
-
const createComponent = (props = {}) => {
wrapper = mount(StageComponent, {
attachTo: document.body,
@@ -31,110 +32,265 @@ describe('Pipelines stage component', () => {
...defaultProps,
...props,
},
+ provide: {
+ glFeatures,
+ },
});
};
beforeEach(() => {
mock = new MockAdapter(axios);
+ jest.spyOn(eventHub, '$emit');
+ glFeatures = {};
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
+ eventHub.$emit.mockRestore();
mock.restore();
});
- describe('default', () => {
- beforeEach(() => {
- createComponent();
+ describe('when ci_mini_pipeline_gl_dropdown feature flag is disabled', () => {
+ const isDropdownOpen = () => wrapper.classes('show');
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should render a dropdown with the status icon', () => {
+ expect(wrapper.attributes('class')).toEqual('dropdown');
+ expect(wrapper.find('svg').exists()).toBe(true);
+ expect(wrapper.find('button').attributes('data-toggle')).toEqual('dropdown');
+ });
});
- it('should render a dropdown with the status icon', () => {
- expect(wrapper.attributes('class')).toEqual('dropdown');
- expect(wrapper.find('svg').exists()).toBe(true);
- expect(wrapper.find('button').attributes('data-toggle')).toEqual('dropdown');
+ describe('with successful request', () => {
+ beforeEach(() => {
+ mock.onGet('path.json').reply(200, stageReply);
+ createComponent();
+ });
+
+ it('should render the received data and emit `clickedDropdown` event', async () => {
+ wrapper.find('button').trigger('click');
+
+ await axios.waitForAll();
+ expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
+ stageReply.latest_statuses[0].name,
+ );
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
+ });
});
- });
- describe('with successful request', () => {
- beforeEach(() => {
- mock.onGet('path.json').reply(200, stageReply);
+ it('when request fails should close the dropdown', async () => {
+ mock.onGet('path.json').reply(500);
createComponent();
- });
+ wrapper.find({ ref: 'dropdown' }).trigger('click');
- it('should render the received data and emit `clickedDropdown` event', async () => {
- jest.spyOn(eventHub, '$emit');
- wrapper.find('button').trigger('click');
+ expect(isDropdownOpen()).toBe(true);
+ wrapper.find('button').trigger('click');
await axios.waitForAll();
- expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
- stageReply.latest_statuses[0].name,
- );
- expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
+ expect(isDropdownOpen()).toBe(false);
});
- });
- it('when request fails should close the dropdown', async () => {
- mock.onGet('path.json').reply(500);
- createComponent();
- wrapper.find({ ref: 'dropdown' }).trigger('click');
- expect(isDropdownOpen()).toBe(true);
+ describe('update endpoint correctly', () => {
+ beforeEach(() => {
+ const copyStage = { ...stageReply };
+ copyStage.latest_statuses[0].name = 'this is the updated content';
+ mock.onGet('bar.json').reply(200, copyStage);
+ createComponent({
+ stage: {
+ status: {
+ group: 'running',
+ icon: 'status_running',
+ title: 'running',
+ },
+ dropdown_path: 'bar.json',
+ },
+ });
+ return axios.waitForAll();
+ });
+
+ it('should update the stage to request the new endpoint provided', async () => {
+ wrapper.find('button').trigger('click');
+ await axios.waitForAll();
+
+ expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
+ 'this is the updated content',
+ );
+ });
+ });
+
+ describe('pipelineActionRequestComplete', () => {
+ beforeEach(() => {
+ mock.onGet('path.json').reply(200, stageReply);
+ mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
+ });
+
+ const clickCiAction = async () => {
+ wrapper.find('button').trigger('click');
+ await axios.waitForAll();
+
+ wrapper.find('.js-ci-action').trigger('click');
+ await axios.waitForAll();
+ };
+
+ describe('within pipeline table', () => {
+ it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => {
+ createComponent({ type: 'PIPELINES_TABLE' });
+
+ await clickCiAction();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
+ });
+ });
+
+ describe('in MR widget', () => {
+ beforeEach(() => {
+ jest.spyOn($.fn, 'dropdown');
+ });
- wrapper.find('button').trigger('click');
- await axios.waitForAll();
+ it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => {
+ createComponent();
- expect(isDropdownOpen()).toBe(false);
+ await clickCiAction();
+
+ expect($.fn.dropdown).toHaveBeenCalledWith('toggle');
+ });
+ });
+ });
});
- describe('update endpoint correctly', () => {
+ describe('when ci_mini_pipeline_gl_dropdown feature flag is enabled', () => {
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findDropdownToggle = () => wrapper.find('button.gl-dropdown-toggle');
+ const findDropdownMenu = () =>
+ wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]');
+ const findCiActionBtn = () => wrapper.find('.js-ci-action');
+
+ const openGlDropdown = () => {
+ findDropdownToggle().trigger('click');
+ return new Promise((resolve) => {
+ wrapper.vm.$root.$on('bv::dropdown::show', resolve);
+ });
+ };
+
beforeEach(() => {
- const copyStage = { ...stageReply };
- copyStage.latest_statuses[0].name = 'this is the updated content';
- mock.onGet('bar.json').reply(200, copyStage);
- createComponent({
- stage: {
- status: {
- group: 'running',
- icon: 'status_running',
- title: 'running',
- },
- dropdown_path: 'bar.json',
- },
+ glFeatures = { ciMiniPipelineGlDropdown: true };
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should render a dropdown with the status icon', () => {
+ expect(findDropdown().exists()).toBe(true);
+ expect(findDropdownToggle().classes('gl-dropdown-toggle')).toEqual(true);
+ expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true);
});
- return axios.waitForAll();
});
- it('should update the stage to request the new endpoint provided', async () => {
- wrapper.find('button').trigger('click');
+ describe('with successful request', () => {
+ beforeEach(() => {
+ mock.onGet('path.json').reply(200, stageReply);
+ createComponent();
+ });
+
+ it('should render the received data and emit `clickedDropdown` event', async () => {
+ await openGlDropdown();
+ await axios.waitForAll();
+
+ expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name);
+ expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
+ });
+ });
+
+ it('when request fails should close the dropdown', async () => {
+ mock.onGet('path.json').reply(500);
+
+ createComponent();
+
+ await openGlDropdown();
await axios.waitForAll();
- expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
- 'this is the updated content',
- );
+ expect(findDropdown().classes('show')).toBe(false);
});
- });
- describe('pipelineActionRequestComplete', () => {
- beforeEach(() => {
- mock.onGet('path.json').reply(200, stageReply);
- mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
+ describe('update endpoint correctly', () => {
+ beforeEach(async () => {
+ const copyStage = { ...stageReply };
+ copyStage.latest_statuses[0].name = 'this is the updated content';
+ mock.onGet('bar.json').reply(200, copyStage);
+ createComponent({
+ stage: {
+ status: {
+ group: 'running',
+ icon: 'status_running',
+ title: 'running',
+ },
+ dropdown_path: 'bar.json',
+ },
+ });
+ await axios.waitForAll();
+ });
- createComponent({ type: 'PIPELINES_TABLE' });
+ it('should update the stage to request the new endpoint provided', async () => {
+ await openGlDropdown();
+ await axios.waitForAll();
+
+ expect(findDropdownMenu().text()).toContain('this is the updated content');
+ });
});
- describe('within pipeline table', () => {
- it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => {
- jest.spyOn(eventHub, '$emit');
+ describe('pipelineActionRequestComplete', () => {
+ beforeEach(() => {
+ mock.onGet('path.json').reply(200, stageReply);
+ mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
+ });
- wrapper.find('button').trigger('click');
+ const clickCiAction = async () => {
+ await openGlDropdown();
await axios.waitForAll();
- wrapper.find('.js-ci-action').trigger('click');
+ findCiActionBtn().trigger('click');
await axios.waitForAll();
+ };
+
+ describe('within pipeline table', () => {
+ beforeEach(() => {
+ createComponent({ type: 'PIPELINES_TABLE' });
+ });
+
+ it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => {
+ await clickCiAction();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
+ });
+ });
+
+ describe('in MR widget', () => {
+ beforeEach(() => {
+ jest.spyOn($.fn, 'dropdown');
+ createComponent();
+ });
+
+ it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => {
+ const hidden = jest.fn();
+
+ wrapper.vm.$root.$on('bv::dropdown::hide', hidden);
+
+ expect(hidden).toHaveBeenCalledTimes(0);
+
+ await clickCiAction();
- expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
+ expect(hidden).toHaveBeenCalledTimes(1);
+ });
});
});
});
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
index f7ff36c0a46..6258b08dfbb 100644
--- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
@@ -2,10 +2,10 @@ import MockAdapter from 'axios-mock-adapter';
import { getJSONFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as actions from '~/pipelines/stores/test_reports/actions';
import * as types from '~/pipelines/stores/test_reports/mutation_types';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash.js');
@@ -16,7 +16,7 @@ describe('Actions TestReports Store', () => {
const testReports = getJSONFixture('pipelines/test_report.json');
const summary = { total_count: 1 };
- const suiteEndpoint = `${TEST_HOST}/tests/:suite_name.json`;
+ const suiteEndpoint = `${TEST_HOST}/tests/suite.json`;
const summaryEndpoint = `${TEST_HOST}/test_reports/summary.json`;
const defaultState = {
suiteEndpoint,
@@ -69,9 +69,8 @@ describe('Actions TestReports Store', () => {
beforeEach(() => {
const buildIds = [1];
testReports.test_suites[0].build_ids = buildIds;
- const endpoint = suiteEndpoint.replace(':suite_name', testReports.test_suites[0].name);
mock
- .onGet(endpoint, { params: { build_ids: buildIds } })
+ .onGet(suiteEndpoint, { params: { build_ids: buildIds } })
.replyOnce(200, testReports.test_suites[0], {});
});
diff --git a/spec/frontend/pipelines/test_reports/stores/getters_spec.js b/spec/frontend/pipelines/test_reports/stores/getters_spec.js
index 7382a6beefa..f8298fdaba5 100644
--- a/spec/frontend/pipelines/test_reports/stores/getters_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/getters_spec.js
@@ -1,6 +1,10 @@
import { getJSONFixture } from 'helpers/fixtures';
import * as getters from '~/pipelines/stores/test_reports/getters';
-import { iconForTestStatus, formattedTime } from '~/pipelines/stores/test_reports/utils';
+import {
+ iconForTestStatus,
+ formatFilePath,
+ formattedTime,
+} from '~/pipelines/stores/test_reports/utils';
describe('Getters TestReports Store', () => {
let state;
@@ -8,6 +12,7 @@ describe('Getters TestReports Store', () => {
const testReports = getJSONFixture('pipelines/test_report.json');
const defaultState = {
+ blobPath: '/test/blob/path',
testReports,
selectedSuiteIndex: 0,
pageInfo: {
@@ -17,6 +22,7 @@ describe('Getters TestReports Store', () => {
};
const emptyState = {
+ blobPath: '',
testReports: {},
selectedSuite: null,
pageInfo: {
@@ -74,6 +80,7 @@ describe('Getters TestReports Store', () => {
const expected = testReports.test_suites[0].test_cases
.map((x) => ({
...x,
+ filePath: `${state.blobPath}/${formatFilePath(x.file)}`,
formattedTime: formattedTime(x.execution_time),
icon: iconForTestStatus(x.status),
}))
@@ -87,6 +94,70 @@ describe('Getters TestReports Store', () => {
expect(getters.getSuiteTests(state)).toEqual([]);
});
+
+ describe('when a test case classname property is null', () => {
+ it('should return an empty string value for the classname property', () => {
+ const testCases = testReports.test_suites[0].test_cases;
+ setupState({
+ ...defaultState,
+ testReports: {
+ ...testReports,
+ test_suites: [
+ {
+ test_cases: testCases.map((testCase) => ({
+ ...testCase,
+ classname: null,
+ })),
+ },
+ ],
+ },
+ });
+
+ const expected = testCases
+ .map((x) => ({
+ ...x,
+ classname: '',
+ filePath: `${state.blobPath}/${formatFilePath(x.file)}`,
+ formattedTime: formattedTime(x.execution_time),
+ icon: iconForTestStatus(x.status),
+ }))
+ .slice(0, state.pageInfo.perPage);
+
+ expect(getters.getSuiteTests(state)).toEqual(expected);
+ });
+ });
+
+ describe('when a test case name property is null', () => {
+ it('should return an empty string value for the name property', () => {
+ const testCases = testReports.test_suites[0].test_cases;
+ setupState({
+ ...defaultState,
+ testReports: {
+ ...testReports,
+ test_suites: [
+ {
+ test_cases: testCases.map((testCase) => ({
+ ...testCase,
+ name: null,
+ })),
+ },
+ ],
+ },
+ });
+
+ const expected = testCases
+ .map((x) => ({
+ ...x,
+ name: '',
+ filePath: `${state.blobPath}/${formatFilePath(x.file)}`,
+ formattedTime: formattedTime(x.execution_time),
+ icon: iconForTestStatus(x.status),
+ }))
+ .slice(0, state.pageInfo.perPage);
+
+ expect(getters.getSuiteTests(state)).toEqual(expected);
+ });
+ });
});
describe('getSuiteTestCount', () => {
diff --git a/spec/frontend/pipelines/test_reports/stores/utils_spec.js b/spec/frontend/pipelines/test_reports/stores/utils_spec.js
index 7e632d099fc..703fe69026c 100644
--- a/spec/frontend/pipelines/test_reports/stores/utils_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/utils_spec.js
@@ -1,6 +1,20 @@
-import { formattedTime } from '~/pipelines/stores/test_reports/utils';
+import { formatFilePath, formattedTime } from '~/pipelines/stores/test_reports/utils';
describe('Test reports utils', () => {
+ describe('formatFilePath', () => {
+ it.each`
+ file | expected
+ ${'./test.js'} | ${'test.js'}
+ ${'/test.js'} | ${'test.js'}
+ ${'.//////////////test.js'} | ${'test.js'}
+ ${'test.js'} | ${'test.js'}
+ ${'mock/path./test.js'} | ${'mock/path./test.js'}
+ ${'./mock/path./test.js'} | ${'mock/path./test.js'}
+ `('should format $file to be $expected', ({ file, expected }) => {
+ expect(formatFilePath(file)).toBe(expected);
+ });
+ });
+
describe('formattedTime', () => {
describe('when time is smaller than a second', () => {
it('should return time in milliseconds fixed to 2 decimals', () => {
diff --git a/spec/frontend/pipelines/test_reports/test_case_details_spec.js b/spec/frontend/pipelines/test_reports/test_case_details_spec.js
index bfb8b43778d..e866586a2c3 100644
--- a/spec/frontend/pipelines/test_reports/test_case_details_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_case_details_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
@@ -11,12 +11,17 @@ describe('Test case details', () => {
classname: 'spec.test_spec',
name: 'Test#something cool',
formattedTime: '10.04ms',
+ recent_failures: {
+ count: 2,
+ base_branch: 'master',
+ },
system_output: 'Line 42 is broken',
};
const findModal = () => wrapper.find(GlModal);
const findName = () => wrapper.find('[data-testid="test-case-name"]');
const findDuration = () => wrapper.find('[data-testid="test-case-duration"]');
+ const findRecentFailures = () => wrapper.find('[data-testid="test-case-recent-failures"]');
const findSystemOutput = () => wrapper.find('[data-testid="test-case-trace"]');
const createComponent = (testCase = {}) => {
@@ -56,6 +61,36 @@ describe('Test case details', () => {
});
});
+ describe('when test case has recent failures', () => {
+ describe('has only 1 recent failure', () => {
+ it('renders the recent failure', () => {
+ createComponent({ recent_failures: { ...defaultTestCase.recent_failures, count: 1 } });
+
+ expect(findRecentFailures().text()).toContain(
+ `Failed 1 time in ${defaultTestCase.recent_failures.base_branch} in the last 14 days`,
+ );
+ });
+ });
+
+ describe('has more than 1 recent failure', () => {
+ it('renders the recent failures', () => {
+ createComponent();
+
+ expect(findRecentFailures().text()).toContain(
+ `Failed ${defaultTestCase.recent_failures.count} times in ${defaultTestCase.recent_failures.base_branch} in the last 14 days`,
+ );
+ });
+ });
+ });
+
+ describe('when test case does not have recent failures', () => {
+ it('does not render the recent failures', () => {
+ createComponent({ recent_failures: null });
+
+ expect(findRecentFailures().exists()).toBe(false);
+ });
+ });
+
describe('when test case has system output', () => {
it('renders the test case system output', () => {
createComponent();
diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js
index c8ab18b9086..da5763ddf8e 100644
--- a/spec/frontend/pipelines/test_reports/test_reports_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js
@@ -1,6 +1,6 @@
-import Vuex from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import { getJSONFixture } from 'helpers/fixtures';
import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
import TestSummary from '~/pipelines/components/test_reports/test_summary.vue';
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 b8fd056610b..a87145cc557 100644
--- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
@@ -1,10 +1,11 @@
-import Vuex from 'vuex';
+import { GlButton, GlFriendlyWrap, GlLink, GlPagination } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import { getJSONFixture } from 'helpers/fixtures';
-import { GlButton, GlFriendlyWrap, GlPagination } from '@gitlab/ui';
import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue';
-import * as getters from '~/pipelines/stores/test_reports/getters';
import { TestStatus } from '~/pipelines/constants';
+import * as getters from '~/pipelines/stores/test_reports/getters';
+import { formatFilePath } from '~/pipelines/stores/test_reports/utils';
import skippedTestCases from './mock_data';
const localVue = createLocalVue();
@@ -20,15 +21,18 @@ describe('Test reports suite table', () => {
testSuite.test_cases = [...testSuite.test_cases, ...skippedTestCases];
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 findLinkForRow = (row) => row.find(GlLink);
const findIconForRow = (row, status) => row.find(`.ci-status-icon-${status}`);
const createComponent = (suite = testSuite, perPage = 20) => {
store = new Vuex.Store({
state: {
+ blobPath,
testReports: {
test_suites: [suite],
},
@@ -64,7 +68,7 @@ describe('Test reports suite table', () => {
beforeEach(() => createComponent());
it('renders the correct number of rows', () => {
- expect(allCaseRows().length).toBe(testCases.length);
+ expect(allCaseRows()).toHaveLength(testCases.length);
});
it.each([
@@ -82,9 +86,13 @@ describe('Test reports suite table', () => {
it('renders the file name for the test with a copy button', () => {
const { file } = testCases[0];
+ const relativeFile = formatFilePath(file);
+ const filePath = `${blobPath}/${relativeFile}`;
const row = findCaseRowAtIndex(0);
+ const fileLink = findLinkForRow(row);
const button = row.find(GlButton);
+ expect(fileLink.attributes('href')).toBe(filePath);
expect(row.text()).toContain(file);
expect(button.exists()).toBe(true);
expect(button.attributes('data-clipboard-text')).toBe(file);
@@ -106,4 +114,32 @@ describe('Test reports suite table', () => {
expect(wrapper.find(GlPagination).exists()).toBe(true);
});
});
+
+ 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,
+ })),
+ });
+
+ expect(allCaseRows()).toHaveLength(testCases.length);
+ });
+ });
+
+ 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,
+ })),
+ });
+
+ expect(allCaseRows()).toHaveLength(testCases.length);
+ });
+ });
});
diff --git a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
index b585536ae09..892a3742fea 100644
--- a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
@@ -1,5 +1,5 @@
-import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import { getJSONFixture } from 'helpers/fixtures';
import SummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue';
import * as getters from '~/pipelines/stores/test_reports/getters';
diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/pipelines/time_ago_spec.js
index b7bc8d08a0f..55a19ef5165 100644
--- a/spec/frontend/pipelines/time_ago_spec.js
+++ b/spec/frontend/pipelines/time_ago_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import TimeAgo from '~/pipelines/components/pipelines_list/time_ago.vue';
describe('Timeago component', () => {
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 371ba5a4f9b..7ddbbb3b005 100644
--- a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
@@ -1,6 +1,6 @@
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
-import { stubComponent } from 'helpers/stub_component';
import { shallowMount } from '@vue/test-utils';
+import { stubComponent } from 'helpers/stub_component';
import Api from '~/api';
import PipelineTriggerAuthorToken from '~/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue';
import { users } from '../mock_data';