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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/pipelines')
-rw-r--r--spec/frontend/pipelines/__snapshots__/parsing_utils_spec.js.snap373
-rw-r--r--spec/frontend/pipelines/components/pipelines_filtered_search_spec.js11
-rw-r--r--spec/frontend/pipelines/graph/graph_component_spec.js1
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js154
-rw-r--r--spec/frontend/pipelines/graph/graph_view_selector_spec.js189
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_column_spec.js21
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_mock_data.js4
-rw-r--r--spec/frontend/pipelines/graph/mock_data.js21
-rw-r--r--spec/frontend/pipelines/graph/mock_data_legacy.js8
-rw-r--r--spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap6
-rw-r--r--spec/frontend/pipelines/graph_shared/links_inner_spec.js155
-rw-r--r--spec/frontend/pipelines/graph_shared/links_layer_spec.js200
-rw-r--r--spec/frontend/pipelines/mock_data.js6
-rw-r--r--spec/frontend/pipelines/parsing_utils_spec.js (renamed from spec/frontend/pipelines/components/dag/parsing_utils_spec.js)46
-rw-r--r--spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js18
-rw-r--r--spec/frontend/pipelines/pipeline_multi_actions_spec.js112
-rw-r--r--spec/frontend/pipelines/pipelines_artifacts_spec.js110
-rw-r--r--spec/frontend/pipelines/pipelines_ci_templates_spec.js2
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js29
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js5
-rw-r--r--spec/frontend/pipelines/test_reports/empty_state_spec.js45
-rw-r--r--spec/frontend/pipelines/test_reports/test_case_details_spec.js55
-rw-r--r--spec/frontend/pipelines/test_reports/test_reports_spec.js45
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js2
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js2
25 files changed, 1325 insertions, 295 deletions
diff --git a/spec/frontend/pipelines/__snapshots__/parsing_utils_spec.js.snap b/spec/frontend/pipelines/__snapshots__/parsing_utils_spec.js.snap
new file mode 100644
index 00000000000..60625d301c0
--- /dev/null
+++ b/spec/frontend/pipelines/__snapshots__/parsing_utils_spec.js.snap
@@ -0,0 +1,373 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DAG visualization parsing utilities generateColumnsFromLayersList matches the snapshot 1`] = `
+Array [
+ Object {
+ "groups": Array [
+ Object {
+ "__typename": "CiGroup",
+ "jobs": Array [
+ Object {
+ "__typename": "CiJob",
+ "name": "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
+ "needs": Array [],
+ "scheduledAt": null,
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "action": Object {
+ "__typename": "StatusAction",
+ "buttonTitle": "Retry this job",
+ "icon": "retry",
+ "path": "/root/abcd-dag/-/jobs/1482/retry",
+ "title": "Retry",
+ },
+ "detailsPath": "/root/abcd-dag/-/jobs/1482",
+ "group": "success",
+ "hasDetails": true,
+ "icon": "status_success",
+ "tooltip": "passed",
+ },
+ },
+ ],
+ "name": "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
+ "size": 1,
+ "stageName": "build",
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "group": "success",
+ "icon": "status_success",
+ "label": "passed",
+ },
+ },
+ Object {
+ "__typename": "CiGroup",
+ "jobs": Array [
+ Object {
+ "__typename": "CiJob",
+ "name": "build_b",
+ "needs": Array [],
+ "scheduledAt": null,
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "action": Object {
+ "__typename": "StatusAction",
+ "buttonTitle": "Retry this job",
+ "icon": "retry",
+ "path": "/root/abcd-dag/-/jobs/1515/retry",
+ "title": "Retry",
+ },
+ "detailsPath": "/root/abcd-dag/-/jobs/1515",
+ "group": "success",
+ "hasDetails": true,
+ "icon": "status_success",
+ "tooltip": "passed",
+ },
+ },
+ ],
+ "name": "build_b",
+ "size": 1,
+ "stageName": "build",
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "group": "success",
+ "icon": "status_success",
+ "label": "passed",
+ },
+ },
+ Object {
+ "__typename": "CiGroup",
+ "jobs": Array [
+ Object {
+ "__typename": "CiJob",
+ "name": "build_c",
+ "needs": Array [],
+ "scheduledAt": null,
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "action": Object {
+ "__typename": "StatusAction",
+ "buttonTitle": "Retry this job",
+ "icon": "retry",
+ "path": "/root/abcd-dag/-/jobs/1484/retry",
+ "title": "Retry",
+ },
+ "detailsPath": "/root/abcd-dag/-/jobs/1484",
+ "group": "success",
+ "hasDetails": true,
+ "icon": "status_success",
+ "tooltip": "passed",
+ },
+ },
+ ],
+ "name": "build_c",
+ "size": 1,
+ "stageName": "build",
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "group": "success",
+ "icon": "status_success",
+ "label": "passed",
+ },
+ },
+ Object {
+ "__typename": "CiGroup",
+ "jobs": Array [
+ Object {
+ "__typename": "CiJob",
+ "name": "build_d 1/3",
+ "needs": Array [],
+ "scheduledAt": null,
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "action": Object {
+ "__typename": "StatusAction",
+ "buttonTitle": "Retry this job",
+ "icon": "retry",
+ "path": "/root/abcd-dag/-/jobs/1485/retry",
+ "title": "Retry",
+ },
+ "detailsPath": "/root/abcd-dag/-/jobs/1485",
+ "group": "success",
+ "hasDetails": true,
+ "icon": "status_success",
+ "tooltip": "passed",
+ },
+ },
+ Object {
+ "__typename": "CiJob",
+ "name": "build_d 2/3",
+ "needs": Array [],
+ "scheduledAt": null,
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "action": Object {
+ "__typename": "StatusAction",
+ "buttonTitle": "Retry this job",
+ "icon": "retry",
+ "path": "/root/abcd-dag/-/jobs/1486/retry",
+ "title": "Retry",
+ },
+ "detailsPath": "/root/abcd-dag/-/jobs/1486",
+ "group": "success",
+ "hasDetails": true,
+ "icon": "status_success",
+ "tooltip": "passed",
+ },
+ },
+ Object {
+ "__typename": "CiJob",
+ "name": "build_d 3/3",
+ "needs": Array [],
+ "scheduledAt": null,
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "action": Object {
+ "__typename": "StatusAction",
+ "buttonTitle": "Retry this job",
+ "icon": "retry",
+ "path": "/root/abcd-dag/-/jobs/1487/retry",
+ "title": "Retry",
+ },
+ "detailsPath": "/root/abcd-dag/-/jobs/1487",
+ "group": "success",
+ "hasDetails": true,
+ "icon": "status_success",
+ "tooltip": "passed",
+ },
+ },
+ ],
+ "name": "build_d",
+ "size": 3,
+ "stageName": "build",
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "group": "success",
+ "icon": "status_success",
+ "label": "passed",
+ },
+ },
+ Object {
+ "__typename": "CiGroup",
+ "jobs": Array [
+ Object {
+ "__typename": "CiJob",
+ "name": "test_c",
+ "needs": Array [],
+ "scheduledAt": null,
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "action": null,
+ "detailsPath": "/root/kinder-pipe/-/pipelines/154",
+ "group": "success",
+ "hasDetails": true,
+ "icon": "status_success",
+ "tooltip": null,
+ },
+ },
+ ],
+ "name": "test_c",
+ "size": 1,
+ "stageName": "test",
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "group": "success",
+ "icon": "status_success",
+ "label": null,
+ },
+ },
+ ],
+ "id": "layer-0",
+ "name": "",
+ "status": Object {
+ "action": null,
+ },
+ },
+ Object {
+ "groups": Array [
+ Object {
+ "__typename": "CiGroup",
+ "jobs": Array [
+ Object {
+ "__typename": "CiJob",
+ "name": "test_a",
+ "needs": Array [
+ "build_c",
+ "build_b",
+ "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
+ ],
+ "scheduledAt": null,
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "action": Object {
+ "__typename": "StatusAction",
+ "buttonTitle": "Retry this job",
+ "icon": "retry",
+ "path": "/root/abcd-dag/-/jobs/1514/retry",
+ "title": "Retry",
+ },
+ "detailsPath": "/root/abcd-dag/-/jobs/1514",
+ "group": "success",
+ "hasDetails": true,
+ "icon": "status_success",
+ "tooltip": "passed",
+ },
+ },
+ ],
+ "name": "test_a",
+ "size": 1,
+ "stageName": "test",
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "group": "success",
+ "icon": "status_success",
+ "label": "passed",
+ },
+ },
+ Object {
+ "__typename": "CiGroup",
+ "jobs": Array [
+ Object {
+ "__typename": "CiJob",
+ "name": "test_b 1/2",
+ "needs": Array [
+ "build_d 3/3",
+ "build_d 2/3",
+ "build_d 1/3",
+ "build_b",
+ "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
+ ],
+ "scheduledAt": null,
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "action": Object {
+ "__typename": "StatusAction",
+ "buttonTitle": "Retry this job",
+ "icon": "retry",
+ "path": "/root/abcd-dag/-/jobs/1489/retry",
+ "title": "Retry",
+ },
+ "detailsPath": "/root/abcd-dag/-/jobs/1489",
+ "group": "success",
+ "hasDetails": true,
+ "icon": "status_success",
+ "tooltip": "passed",
+ },
+ },
+ Object {
+ "__typename": "CiJob",
+ "name": "test_b 2/2",
+ "needs": Array [
+ "build_d 3/3",
+ "build_d 2/3",
+ "build_d 1/3",
+ "build_b",
+ "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
+ ],
+ "scheduledAt": null,
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "action": Object {
+ "__typename": "StatusAction",
+ "buttonTitle": "Retry this job",
+ "icon": "retry",
+ "path": "/root/abcd-dag/-/jobs/1490/retry",
+ "title": "Retry",
+ },
+ "detailsPath": "/root/abcd-dag/-/jobs/1490",
+ "group": "success",
+ "hasDetails": true,
+ "icon": "status_success",
+ "tooltip": "passed",
+ },
+ },
+ ],
+ "name": "test_b",
+ "size": 2,
+ "stageName": "test",
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "group": "success",
+ "icon": "status_success",
+ "label": "passed",
+ },
+ },
+ Object {
+ "__typename": "CiGroup",
+ "jobs": Array [
+ Object {
+ "__typename": "CiJob",
+ "name": "test_d",
+ "needs": Array [
+ "build_b",
+ ],
+ "scheduledAt": null,
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "action": null,
+ "detailsPath": "/root/abcd-dag/-/pipelines/153",
+ "group": "success",
+ "hasDetails": true,
+ "icon": "status_success",
+ "tooltip": null,
+ },
+ },
+ ],
+ "name": "test_d",
+ "size": 1,
+ "stageName": "test",
+ "status": Object {
+ "__typename": "DetailedStatus",
+ "group": "success",
+ "icon": "status_success",
+ "label": null,
+ },
+ },
+ ],
+ "id": "layer-1",
+ "name": "",
+ "status": Object {
+ "action": null,
+ },
+ },
+]
+`;
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
index e43aa2a02f5..b0dbba37b94 100644
--- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
@@ -4,6 +4,7 @@ import MockAdapter from 'axios-mock-adapter';
import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import PipelinesFilteredSearch from '~/pipelines/components/pipelines_list/pipelines_filtered_search.vue';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import { users, mockSearch, branches, tags } from '../mock_data';
describe('Pipelines filtered search', () => {
@@ -57,7 +58,7 @@ describe('Pipelines filtered search', () => {
title: 'Trigger author',
unique: true,
projectId: '21',
- operators: [expect.objectContaining({ value: '=' })],
+ operators: OPERATOR_IS_ONLY,
});
expect(findBranchToken()).toMatchObject({
@@ -66,7 +67,7 @@ describe('Pipelines filtered search', () => {
title: 'Branch name',
unique: true,
projectId: '21',
- operators: [expect.objectContaining({ value: '=' })],
+ operators: OPERATOR_IS_ONLY,
});
expect(findStatusToken()).toMatchObject({
@@ -74,7 +75,7 @@ describe('Pipelines filtered search', () => {
icon: 'status',
title: 'Status',
unique: true,
- operators: [expect.objectContaining({ value: '=' })],
+ operators: OPERATOR_IS_ONLY,
});
expect(findTagToken()).toMatchObject({
@@ -82,7 +83,7 @@ describe('Pipelines filtered search', () => {
icon: 'tag',
title: 'Tag name',
unique: true,
- operators: [expect.objectContaining({ value: '=' })],
+ operators: OPERATOR_IS_ONLY,
});
});
@@ -138,7 +139,7 @@ describe('Pipelines filtered search', () => {
describe('Url query params', () => {
const params = {
username: 'deja.green',
- ref: 'master',
+ ref: 'main',
};
beforeEach(() => {
diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js
index e8fb036368a..30914ba99a5 100644
--- a/spec/frontend/pipelines/graph/graph_component_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_spec.js
@@ -22,6 +22,7 @@ describe('graph component', () => {
const defaultProps = {
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
+ showLinks: false,
viewType: STAGE_VIEW,
configPaths: {
metricsPath: '',
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 8c469966be4..4914a9a1ced 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -15,8 +15,10 @@ import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
+import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import * as parsingUtils from '~/pipelines/components/parsing_utils';
-import { mockPipelineResponse } from './mock_data';
+import getUserCallouts from '~/pipelines/graphql/queries/get_user_callouts.query.graphql';
+import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data';
const defaultProvide = {
graphqlResourceEtag: 'frog/amphibirama/etag/',
@@ -30,13 +32,16 @@ describe('Pipeline graph wrapper', () => {
useLocalStorageSpy();
let wrapper;
- const getAlert = () => wrapper.find(GlAlert);
- const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const getAlert = () => wrapper.findComponent(GlAlert);
+ const getDependenciesToggle = () => wrapper.find('[data-testid="show-links-toggle"]');
+ const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const getLinksLayer = () => wrapper.findComponent(LinksLayer);
const getGraph = () => wrapper.find(PipelineGraph);
const getStageColumnTitle = () => wrapper.find('[data-testid="stage-column-title"]');
const getAllStageColumnGroupsInColumn = () =>
wrapper.find(StageColumnComponent).findAll('[data-testid="stage-column-group"]');
const getViewSelector = () => wrapper.find(GraphViewSelector);
+ const getViewSelectorTrip = () => getViewSelector().findComponent(GlAlert);
const createComponent = ({
apolloProvider,
@@ -59,14 +64,22 @@ describe('Pipeline graph wrapper', () => {
};
const createComponentWithApollo = ({
+ calloutsList = [],
+ data = {},
getPipelineDetailsHandler = jest.fn().mockResolvedValue(mockPipelineResponse),
mountFn = shallowMount,
provide = {},
} = {}) => {
- const requestHandlers = [[getPipelineDetails, getPipelineDetailsHandler]];
+ const callouts = mapCallouts(calloutsList);
+ const getUserCalloutsHandler = jest.fn().mockResolvedValue(mockCalloutsResponse(callouts));
+
+ const requestHandlers = [
+ [getPipelineDetails, getPipelineDetailsHandler],
+ [getUserCallouts, getUserCalloutsHandler],
+ ];
const apolloProvider = createMockApollo(requestHandlers);
- createComponent({ apolloProvider, provide, mountFn });
+ createComponent({ apolloProvider, data, provide, mountFn });
};
afterEach(() => {
@@ -74,6 +87,15 @@ describe('Pipeline graph wrapper', () => {
wrapper = null;
});
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
+ });
+
describe('when data is loading', () => {
it('displays the loading icon', () => {
createComponentWithApollo();
@@ -282,6 +304,87 @@ describe('Pipeline graph wrapper', () => {
});
});
+ describe('when pipelineGraphLayersView feature flag is on and layers view is selected', () => {
+ beforeEach(async () => {
+ createComponentWithApollo({
+ provide: {
+ glFeatures: {
+ pipelineGraphLayersView: true,
+ },
+ },
+ data: {
+ currentViewType: LAYER_VIEW,
+ },
+ mountFn: mount,
+ });
+
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+ });
+
+ it('sets showLinks to true', async () => {
+ /* This spec uses .props for performance reasons. */
+ expect(getLinksLayer().exists()).toBe(true);
+ expect(getLinksLayer().props('showLinks')).toBe(false);
+ expect(getViewSelector().props('type')).toBe(LAYER_VIEW);
+ await getDependenciesToggle().trigger('click');
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+ expect(wrapper.findComponent(LinksLayer).props('showLinks')).toBe(true);
+ });
+ });
+
+ describe('when pipelineGraphLayersView feature flag is on, layers view is selected, and links are active', () => {
+ beforeEach(async () => {
+ createComponentWithApollo({
+ provide: {
+ glFeatures: {
+ pipelineGraphLayersView: true,
+ },
+ },
+ data: {
+ currentViewType: LAYER_VIEW,
+ showLinks: true,
+ },
+ mountFn: mount,
+ });
+
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+ });
+
+ it('shows the hover tip in the view selector', async () => {
+ await getViewSelector().setData({ showLinksActive: true });
+ expect(getViewSelectorTrip().exists()).toBe(true);
+ });
+ });
+
+ describe('when hover tip would otherwise show, but it has been previously dismissed', () => {
+ beforeEach(async () => {
+ createComponentWithApollo({
+ provide: {
+ glFeatures: {
+ pipelineGraphLayersView: true,
+ },
+ },
+ data: {
+ currentViewType: LAYER_VIEW,
+ showLinks: true,
+ },
+ mountFn: mount,
+ calloutsList: ['pipeline_needs_hover_tip'.toUpperCase()],
+ });
+
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+ });
+
+ it('does not show the hover tip', async () => {
+ await getViewSelector().setData({ showLinksActive: true });
+ expect(getViewSelectorTrip().exists()).toBe(false);
+ });
+ });
+
describe('when feature flag is on and local storage is set', () => {
beforeEach(async () => {
localStorage.setItem(VIEW_TYPE_KEY, LAYER_VIEW);
@@ -299,10 +402,45 @@ describe('Pipeline graph wrapper', () => {
await wrapper.vm.$nextTick();
});
+ afterEach(() => {
+ localStorage.clear();
+ });
+
it('reads the view type from localStorage when available', () => {
- expect(wrapper.find('[data-testid="pipeline-view-selector"] code').text()).toContain(
- 'needs:',
- );
+ const viewSelectorNeedsSegment = wrapper
+ .findAll('[data-testid="pipeline-view-selector"] > label')
+ .at(1);
+ expect(viewSelectorNeedsSegment.classes()).toContain('active');
+ });
+ });
+
+ describe('when feature flag is on and local storage is set, but the graph does not use needs', () => {
+ beforeEach(async () => {
+ const nonNeedsResponse = { ...mockPipelineResponse };
+ nonNeedsResponse.data.project.pipeline.usesNeeds = false;
+
+ localStorage.setItem(VIEW_TYPE_KEY, LAYER_VIEW);
+
+ createComponentWithApollo({
+ provide: {
+ glFeatures: {
+ pipelineGraphLayersView: true,
+ },
+ },
+ mountFn: mount,
+ getPipelineDetailsHandler: jest.fn().mockResolvedValue(nonNeedsResponse),
+ });
+
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+ });
+
+ afterEach(() => {
+ localStorage.clear();
+ });
+
+ it('still passes stage type to graph', () => {
+ expect(getGraph().props('viewType')).toBe(STAGE_VIEW);
});
});
diff --git a/spec/frontend/pipelines/graph/graph_view_selector_spec.js b/spec/frontend/pipelines/graph/graph_view_selector_spec.js
new file mode 100644
index 00000000000..5b2a29de443
--- /dev/null
+++ b/spec/frontend/pipelines/graph/graph_view_selector_spec.js
@@ -0,0 +1,189 @@
+import { GlAlert, GlLoadingIcon, GlSegmentedControl } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import { LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
+import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
+
+describe('the graph view selector component', () => {
+ let wrapper;
+
+ const findDependenciesToggle = () => wrapper.find('[data-testid="show-links-toggle"]');
+ const findViewTypeSelector = () => wrapper.findComponent(GlSegmentedControl);
+ const findStageViewLabel = () => findViewTypeSelector().findAll('label').at(0);
+ const findLayersViewLabel = () => findViewTypeSelector().findAll('label').at(1);
+ const findSwitcherLoader = () => wrapper.find('[data-testid="switcher-loading-state"]');
+ const findToggleLoader = () => findDependenciesToggle().find(GlLoadingIcon);
+ const findHoverTip = () => wrapper.findComponent(GlAlert);
+
+ const defaultProps = {
+ showLinks: false,
+ tipPreviouslyDismissed: false,
+ type: STAGE_VIEW,
+ };
+
+ const defaultData = {
+ hoverTipDismissed: false,
+ isToggleLoading: false,
+ isSwitcherLoading: false,
+ showLinksActive: false,
+ };
+
+ const createComponent = ({ data = {}, mountFn = shallowMount, props = {} } = {}) => {
+ wrapper = mountFn(GraphViewSelector, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ data() {
+ return {
+ ...defaultData,
+ ...data,
+ };
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when showing stage view', () => {
+ beforeEach(() => {
+ createComponent({ mountFn: mount });
+ });
+
+ it('shows the Stage view label as active in the selector', () => {
+ expect(findStageViewLabel().classes()).toContain('active');
+ });
+
+ it('does not show the Job dependencies (links) toggle', () => {
+ expect(findDependenciesToggle().exists()).toBe(false);
+ });
+ });
+
+ describe('when showing Job dependencies view', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mount,
+ props: {
+ type: LAYER_VIEW,
+ },
+ });
+ });
+
+ it('shows the Job dependencies view label as active in the selector', () => {
+ expect(findLayersViewLabel().classes()).toContain('active');
+ });
+
+ it('shows the Job dependencies (links) toggle', () => {
+ expect(findDependenciesToggle().exists()).toBe(true);
+ });
+ });
+
+ describe('events', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ createComponent({
+ mountFn: mount,
+ props: {
+ type: LAYER_VIEW,
+ },
+ });
+ });
+
+ it('shows loading state and emits updateViewType when view type toggled', async () => {
+ expect(wrapper.emitted().updateViewType).toBeUndefined();
+ expect(findSwitcherLoader().exists()).toBe(false);
+
+ await findStageViewLabel().trigger('click');
+ /*
+ Loading happens before the event is emitted or timers are run.
+ Then we run the timer because the event is emitted in setInterval
+ which is what gives the loader a chace to show up.
+ */
+ expect(findSwitcherLoader().exists()).toBe(true);
+ jest.runOnlyPendingTimers();
+
+ expect(wrapper.emitted().updateViewType).toHaveLength(1);
+ expect(wrapper.emitted().updateViewType).toEqual([[STAGE_VIEW]]);
+ });
+
+ it('shows loading state and emits updateShowLinks when show links toggle is clicked', async () => {
+ expect(wrapper.emitted().updateShowLinksState).toBeUndefined();
+ expect(findToggleLoader().exists()).toBe(false);
+
+ await findDependenciesToggle().trigger('click');
+ /*
+ Loading happens before the event is emitted or timers are run.
+ Then we run the timer because the event is emitted in setInterval
+ which is what gives the loader a chace to show up.
+ */
+ expect(findToggleLoader().exists()).toBe(true);
+ jest.runOnlyPendingTimers();
+
+ expect(wrapper.emitted().updateShowLinksState).toHaveLength(1);
+ expect(wrapper.emitted().updateShowLinksState).toEqual([[true]]);
+ });
+ });
+
+ describe('hover tip callout', () => {
+ describe('when links are live and it has not been previously dismissed', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ showLinks: true,
+ },
+ data: {
+ showLinksActive: true,
+ },
+ mountFn: mount,
+ });
+ });
+
+ it('is displayed', () => {
+ expect(findHoverTip().exists()).toBe(true);
+ expect(findHoverTip().text()).toBe(wrapper.vm.$options.i18n.hoverTipText);
+ });
+
+ it('emits dismissHoverTip event when the tip is dismissed', async () => {
+ expect(wrapper.emitted().dismissHoverTip).toBeUndefined();
+ await findHoverTip().find('button').trigger('click');
+ expect(wrapper.emitted().dismissHoverTip).toHaveLength(1);
+ });
+ });
+
+ describe('when links are live and it has been previously dismissed', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ showLinks: true,
+ tipPreviouslyDismissed: true,
+ },
+ data: {
+ showLinksActive: true,
+ },
+ });
+ });
+
+ it('is not displayed', () => {
+ expect(findHoverTip().exists()).toBe(false);
+ });
+ });
+
+ describe('when links are not live', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ showLinks: true,
+ },
+ data: {
+ showLinksActive: false,
+ },
+ });
+ });
+
+ it('is not displayed', () => {
+ expect(findHoverTip().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
index 8aecfc1b649..24cc6e76098 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
@@ -26,6 +26,7 @@ describe('Linked Pipelines Column', () => {
const defaultProps = {
columnTitle: 'Downstream',
linkedPipelines: processedPipeline.downstream,
+ showLinks: false,
type: DOWNSTREAM,
viewType: STAGE_VIEW,
configPaths: {
@@ -120,6 +121,26 @@ describe('Linked Pipelines Column', () => {
});
});
+ describe('when graph does not use needs', () => {
+ beforeEach(() => {
+ const nonNeedsResponse = { ...wrappedPipelineReturn };
+ nonNeedsResponse.data.project.pipeline.usesNeeds = false;
+
+ createComponentWithApollo({
+ props: {
+ viewType: LAYER_VIEW,
+ },
+ getPipelineDetailsHandler: jest.fn().mockResolvedValue(nonNeedsResponse),
+ mountFn: mount,
+ });
+ });
+
+ it('shows the stage view, even when the main graph view type is layers', async () => {
+ await clickExpandButtonAndAwaitTimers();
+ expect(findPipelineGraph().props('viewType')).toBe(STAGE_VIEW);
+ });
+ });
+
describe('downstream', () => {
describe('when successful', () => {
beforeEach(() => {
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js
index 5756a666ff3..eb05669463b 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js
@@ -3727,8 +3727,8 @@ export default {
scheduled_actions: [],
},
ref: {
- name: 'master',
- path: '/h5bp/html5-boilerplate/commits/master',
+ name: 'main',
+ path: '/h5bp/html5-boilerplate/commits/main',
tag: false,
branch: true,
merge_request: false,
diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js
index cf420f68f37..28fe3b67e7b 100644
--- a/spec/frontend/pipelines/graph/mock_data.js
+++ b/spec/frontend/pipelines/graph/mock_data.js
@@ -8,6 +8,7 @@ export const mockPipelineResponse = {
__typename: 'Pipeline',
id: 163,
iid: '22',
+ complete: true,
usesNeeds: true,
downstream: null,
upstream: null,
@@ -570,6 +571,7 @@ export const wrappedPipelineReturn = {
__typename: 'Pipeline',
id: 'gid://gitlab/Ci::Pipeline/175',
iid: '38',
+ complete: true,
usesNeeds: true,
downstream: {
__typename: 'PipelineConnection',
@@ -669,3 +671,22 @@ export const pipelineWithUpstreamDownstream = (base) => {
return generateResponse(pip, 'root/abcd-dag');
};
+
+export const mapCallouts = (callouts) =>
+ callouts.map((callout) => {
+ return { featureName: callout, __typename: 'UserCallout' };
+ });
+
+export const mockCalloutsResponse = (mappedCallouts) => ({
+ data: {
+ currentUser: {
+ id: 45,
+ __typename: 'User',
+ callouts: {
+ id: 5,
+ __typename: 'UserCalloutConnection',
+ nodes: mappedCallouts,
+ },
+ },
+ },
+});
diff --git a/spec/frontend/pipelines/graph/mock_data_legacy.js b/spec/frontend/pipelines/graph/mock_data_legacy.js
index a4a5d78f906..e1c8b027121 100644
--- a/spec/frontend/pipelines/graph/mock_data_legacy.js
+++ b/spec/frontend/pipelines/graph/mock_data_legacy.js
@@ -221,22 +221,22 @@ export default {
cancelable: false,
},
ref: {
- name: 'master',
- path: '/root/ci-mock/tree/master',
+ name: 'main',
+ path: '/root/ci-mock/tree/main',
tag: false,
branch: true,
},
commit: {
id: '798e5f902592192afaba73f4668ae30e56eae492',
short_id: '798e5f90',
- title: "Merge branch 'new-branch' into 'master'\r",
+ title: "Merge branch 'new-branch' into 'main'\r",
created_at: '2017-04-13T10:25:17.000+01:00',
parent_ids: [
'54d483b1ed156fbbf618886ddf7ab023e24f8738',
'c8e2d38a6c538822e81c57022a6e3a0cfedebbcc',
],
message:
- "Merge branch 'new-branch' into 'master'\r\n\r\nAdd new file\r\n\r\nSee merge request !1",
+ "Merge branch 'new-branch' into 'main'\r\n\r\nAdd new file\r\n\r\nSee merge request !1",
author_name: 'Root',
author_email: 'admin@example.com',
authored_date: '2017-04-13T10:25:17.000+01:00',
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
index cf2b66dea5f..c67b91ae190 100644
--- a/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap
+++ b/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap
@@ -1,7 +1,7 @@
// 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\\">
+"<div class=\\"gl-display-flex gl-relative\\" totalgroups=\\"10\\"><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>
@@ -11,13 +11,13 @@ exports[`Links Inner component with a large number of needs matches snapshot and
`;
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\\">
+"<div class=\\"gl-display-flex gl-relative\\" totalgroups=\\"10\\"><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\\">
+"<div class=\\"gl-display-flex gl-relative\\" totalgroups=\\"10\\"><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
index e81f046c1eb..bb1f0965469 100644
--- a/spec/frontend/pipelines/graph_shared/links_inner_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
@@ -1,16 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
import { setHTMLFixture } from 'helpers/fixtures';
-import axios from '~/lib/utils/axios_utils';
-import {
- PIPELINES_DETAIL_LINK_DURATION,
- PIPELINES_DETAIL_LINKS_TOTAL,
- PIPELINES_DETAIL_LINKS_JOB_RATIO,
-} from '~/performance/constants';
-import * as perfUtils from '~/performance/utils';
-import * as Api from '~/pipelines/components/graph_shared/api';
import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
-import * as sentryUtils from '~/pipelines/utils';
+import { parseData } from '~/pipelines/components/parsing_utils';
import { createJobsHash } from '~/pipelines/utils';
import {
jobRect,
@@ -34,8 +25,13 @@ describe('Links Inner component', () => {
let wrapper;
const createComponent = (props) => {
+ const currentPipelineData = props?.pipelineData || defaultProps.pipelineData;
wrapper = shallowMount(LinksInner, {
- propsData: { ...defaultProps, ...props },
+ propsData: {
+ ...defaultProps,
+ ...props,
+ parsedData: parseData(currentPipelineData.flatMap(({ groups }) => groups)),
+ },
});
};
@@ -206,141 +202,4 @@ describe('Links Inner component', () => {
expect(firstLink.classes(hoverColorClass)).toBe(true);
});
});
-
- describe('performance metrics', () => {
- let markAndMeasure;
- let reportToSentry;
- let reportPerformance;
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => cb());
- markAndMeasure = jest.spyOn(perfUtils, 'performanceMarkAndMeasure');
- reportToSentry = jest.spyOn(sentryUtils, 'reportToSentry');
- reportPerformance = jest.spyOn(Api, 'reportPerformance');
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('with no metrics config object', () => {
- beforeEach(() => {
- setFixtures(pipelineData);
- createComponent({
- pipelineData: pipelineData.stages,
- });
- });
-
- it('is not called', () => {
- expect(markAndMeasure).not.toHaveBeenCalled();
- expect(reportToSentry).not.toHaveBeenCalled();
- expect(reportPerformance).not.toHaveBeenCalled();
- });
- });
-
- describe('with metrics config set to false', () => {
- beforeEach(() => {
- setFixtures(pipelineData);
- createComponent({
- pipelineData: pipelineData.stages,
- metricsConfig: {
- collectMetrics: false,
- metricsPath: '/path/to/metrics',
- },
- });
- });
-
- it('is not called', () => {
- expect(markAndMeasure).not.toHaveBeenCalled();
- expect(reportToSentry).not.toHaveBeenCalled();
- expect(reportPerformance).not.toHaveBeenCalled();
- });
- });
-
- describe('with no metrics path', () => {
- beforeEach(() => {
- setFixtures(pipelineData);
- createComponent({
- pipelineData: pipelineData.stages,
- metricsConfig: {
- collectMetrics: true,
- metricsPath: '',
- },
- });
- });
-
- it('is not called', () => {
- expect(markAndMeasure).not.toHaveBeenCalled();
- expect(reportToSentry).not.toHaveBeenCalled();
- expect(reportPerformance).not.toHaveBeenCalled();
- });
- });
-
- describe('with metrics path and collect set to true', () => {
- const metricsPath = '/root/project/-/ci/prometheus_metrics/histograms.json';
- const duration = 0.0478;
- const numLinks = 1;
- const metricsData = {
- histograms: [
- { name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 },
- { name: PIPELINES_DETAIL_LINKS_TOTAL, value: numLinks },
- {
- name: PIPELINES_DETAIL_LINKS_JOB_RATIO,
- value: numLinks / defaultProps.totalGroups,
- },
- ],
- };
-
- describe('when no duration is obtained', () => {
- beforeEach(() => {
- jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => {
- return [];
- });
-
- setFixtures(pipelineData);
-
- createComponent({
- pipelineData: pipelineData.stages,
- metricsConfig: {
- collectMetrics: true,
- path: metricsPath,
- },
- });
- });
-
- it('attempts to collect metrics', () => {
- expect(markAndMeasure).toHaveBeenCalled();
- expect(reportPerformance).not.toHaveBeenCalled();
- expect(reportToSentry).not.toHaveBeenCalled();
- });
- });
-
- describe('with duration and no error', () => {
- beforeEach(() => {
- jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => {
- return [{ duration }];
- });
-
- setFixtures(pipelineData);
-
- createComponent({
- pipelineData: pipelineData.stages,
- metricsConfig: {
- collectMetrics: true,
- path: metricsPath,
- },
- });
- });
-
- it('it calls reportPerformance with expected arguments', () => {
- expect(markAndMeasure).toHaveBeenCalled();
- expect(reportPerformance).toHaveBeenCalled();
- expect(reportPerformance).toHaveBeenCalledWith(metricsPath, metricsData);
- expect(reportToSentry).not.toHaveBeenCalled();
- });
- });
- });
- });
});
diff --git a/spec/frontend/pipelines/graph_shared/links_layer_spec.js b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
index 5e5365eef30..932a19f2f00 100644
--- a/spec/frontend/pipelines/graph_shared/links_layer_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
@@ -1,32 +1,33 @@
-import { GlAlert } from '@gitlab/ui';
-import { fireEvent, within } from '@testing-library/dom';
-import { mount, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import {
+ PIPELINES_DETAIL_LINK_DURATION,
+ PIPELINES_DETAIL_LINKS_TOTAL,
+ PIPELINES_DETAIL_LINKS_JOB_RATIO,
+} from '~/performance/constants';
+import * as perfUtils from '~/performance/utils';
+import * as Api from '~/pipelines/components/graph_shared/api';
import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
+import * as sentryUtils from '~/pipelines/utils';
import { generateResponse, mockPipelineResponse } from '../graph/mock_data';
describe('links layer component', () => {
let wrapper;
- const withinComponent = () => within(wrapper.element);
- const findAlert = () => wrapper.find(GlAlert);
- const findShowAnyways = () =>
- withinComponent().getByText(wrapper.vm.$options.i18n.showLinksAnyways);
const findLinksInner = () => wrapper.find(LinksInner);
const pipeline = generateResponse(mockPipelineResponse, 'root/fungi-xoxo');
const containerId = `pipeline-links-container-${pipeline.id}`;
const slotContent = "<div>Ceci n'est pas un graphique</div>";
- const tooManyStages = Array(101)
- .fill(0)
- .flatMap(() => pipeline.stages);
-
const defaultProps = {
containerId,
containerMeasurements: { width: 400, height: 400 },
pipelineId: pipeline.id,
pipelineData: pipeline.stages,
+ showLinks: false,
};
const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => {
@@ -46,10 +47,9 @@ describe('links layer component', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
- describe('with data under max stages', () => {
+ describe('with show links off', () => {
beforeEach(() => {
createComponent();
});
@@ -58,62 +58,174 @@ describe('links layer component', () => {
expect(wrapper.html()).toContain(slotContent);
});
+ it('does not render inner links component', () => {
+ expect(findLinksInner().exists()).toBe(false);
+ });
+ });
+
+ describe('with show links on', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ showLinks: true,
+ },
+ });
+ });
+
+ it('renders the default slot', () => {
+ expect(wrapper.html()).toContain(slotContent);
+ });
+
it('renders the inner links component', () => {
expect(findLinksInner().exists()).toBe(true);
});
});
- describe('with more than the max number of stages', () => {
- describe('rendering', () => {
- beforeEach(() => {
- createComponent({ props: { pipelineData: tooManyStages } });
- });
+ describe('with width or height measurement at 0', () => {
+ beforeEach(() => {
+ createComponent({ props: { containerMeasurements: { width: 0, height: 100 } } });
+ });
- it('renders the default slot', () => {
- expect(wrapper.html()).toContain(slotContent);
- });
+ it('renders the default slot', () => {
+ expect(wrapper.html()).toContain(slotContent);
+ });
- it('renders the alert component', () => {
- expect(findAlert().exists()).toBe(true);
- });
+ it('does not render the inner links component', () => {
+ expect(findLinksInner().exists()).toBe(false);
+ });
+ });
- it('does not render the inner links component', () => {
- expect(findLinksInner().exists()).toBe(false);
- });
+ describe('performance metrics', () => {
+ const metricsPath = '/root/project/-/ci/prometheus_metrics/histograms.json';
+ let markAndMeasure;
+ let reportToSentry;
+ let reportPerformance;
+ let mock;
+
+ beforeEach(() => {
+ jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => cb());
+ markAndMeasure = jest.spyOn(perfUtils, 'performanceMarkAndMeasure');
+ reportToSentry = jest.spyOn(sentryUtils, 'reportToSentry');
+ reportPerformance = jest.spyOn(Api, 'reportPerformance');
});
- describe('with width or height measurement at 0', () => {
+ describe('with no metrics config object', () => {
beforeEach(() => {
- createComponent({ props: { containerMeasurements: { width: 0, height: 100 } } });
+ createComponent();
});
- it('renders the default slot', () => {
- expect(wrapper.html()).toContain(slotContent);
+ it('is not called', () => {
+ expect(markAndMeasure).not.toHaveBeenCalled();
+ expect(reportToSentry).not.toHaveBeenCalled();
+ expect(reportPerformance).not.toHaveBeenCalled();
});
+ });
- it('does not render the alert component', () => {
- expect(findAlert().exists()).toBe(false);
+ describe('with metrics config set to false', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ metricsConfig: {
+ collectMetrics: false,
+ metricsPath: '/path/to/metrics',
+ },
+ },
+ });
});
- it('does not render the inner links component', () => {
- expect(findLinksInner().exists()).toBe(false);
+ it('is not called', () => {
+ expect(markAndMeasure).not.toHaveBeenCalled();
+ expect(reportToSentry).not.toHaveBeenCalled();
+ expect(reportPerformance).not.toHaveBeenCalled();
});
});
- describe('interactions', () => {
+ describe('with no metrics path', () => {
beforeEach(() => {
- createComponent({ mountFn: mount, props: { pipelineData: tooManyStages } });
+ createComponent({
+ props: {
+ metricsConfig: {
+ collectMetrics: true,
+ metricsPath: '',
+ },
+ },
+ });
});
- it('renders the disable button', () => {
- expect(findShowAnyways()).not.toBe(null);
+ it('is not called', () => {
+ expect(markAndMeasure).not.toHaveBeenCalled();
+ expect(reportToSentry).not.toHaveBeenCalled();
+ expect(reportPerformance).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('with metrics path and collect set to true', () => {
+ const duration = 875;
+ const numLinks = 7;
+ const totalGroups = 8;
+ const metricsData = {
+ histograms: [
+ { name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 },
+ { name: PIPELINES_DETAIL_LINKS_TOTAL, value: numLinks },
+ {
+ name: PIPELINES_DETAIL_LINKS_JOB_RATIO,
+ value: numLinks / totalGroups,
+ },
+ ],
+ };
+
+ describe('when no duration is obtained', () => {
+ beforeEach(() => {
+ jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => {
+ return [];
+ });
+
+ createComponent({
+ props: {
+ metricsConfig: {
+ collectMetrics: true,
+ path: metricsPath,
+ },
+ },
+ });
+ });
+
+ it('attempts to collect metrics', () => {
+ expect(markAndMeasure).toHaveBeenCalled();
+ expect(reportPerformance).not.toHaveBeenCalled();
+ expect(reportToSentry).not.toHaveBeenCalled();
+ });
});
- it('shows links when override is clicked', async () => {
- expect(findLinksInner().exists()).toBe(false);
- fireEvent(findShowAnyways(), new MouseEvent('click', { bubbles: true }));
- await wrapper.vm.$nextTick();
- expect(findLinksInner().exists()).toBe(true);
+ describe('with duration and no error', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onPost(metricsPath).reply(200, {});
+
+ jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => {
+ return [{ duration }];
+ });
+
+ createComponent({
+ props: {
+ metricsConfig: {
+ collectMetrics: true,
+ path: metricsPath,
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('it calls reportPerformance with expected arguments', () => {
+ expect(markAndMeasure).toHaveBeenCalled();
+ expect(reportPerformance).toHaveBeenCalled();
+ expect(reportPerformance).toHaveBeenCalledWith(metricsPath, metricsData);
+ expect(reportToSentry).not.toHaveBeenCalled();
+ });
});
});
});
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js
index 337838c41b3..16f15b20824 100644
--- a/spec/frontend/pipelines/mock_data.js
+++ b/spec/frontend/pipelines/mock_data.js
@@ -387,7 +387,7 @@ export const tags = [
protected: false,
},
{
- name: 'master-tag',
+ name: 'main-tag',
message: '',
target: '66673b07efef254dab7d537f0433a40e61cf84fe',
commit: {
@@ -413,10 +413,10 @@ export const tags = [
export const mockSearch = [
{ type: 'username', value: { data: 'root', operator: '=' } },
- { type: 'ref', value: { data: 'master', operator: '=' } },
+ { type: 'ref', value: { data: 'main', operator: '=' } },
{ type: 'status', value: { data: 'pending', operator: '=' } },
];
export const mockBranchesAfterMap = ['branch-1', 'branch-10', 'branch-11'];
-export const mockTagsAfterMap = ['tag-3', 'tag-2', 'tag-1', 'master-tag'];
+export const mockTagsAfterMap = ['tag-3', 'tag-2', 'tag-1', 'main-tag'];
diff --git a/spec/frontend/pipelines/components/dag/parsing_utils_spec.js b/spec/frontend/pipelines/parsing_utils_spec.js
index 84ff83883b7..96748ae9e5c 100644
--- a/spec/frontend/pipelines/components/dag/parsing_utils_spec.js
+++ b/spec/frontend/pipelines/parsing_utils_spec.js
@@ -3,12 +3,15 @@ import {
createNodeDict,
makeLinksFromNodes,
filterByAncestors,
+ generateColumnsFromLayersListBare,
+ listByLayers,
parseData,
removeOrphanNodes,
getMaxNodes,
} from '~/pipelines/components/parsing_utils';
-import { mockParsedGraphQLNodes } from './mock_data';
+import { mockParsedGraphQLNodes } from './components/dag/mock_data';
+import { generateResponse, mockPipelineResponse } from './graph/mock_data';
describe('DAG visualization parsing utilities', () => {
const nodeDict = createNodeDict(mockParsedGraphQLNodes);
@@ -108,4 +111,45 @@ describe('DAG visualization parsing utilities', () => {
expect(getMaxNodes(layerNodes)).toBe(3);
});
});
+
+ describe('generateColumnsFromLayersList', () => {
+ const pipeline = generateResponse(mockPipelineResponse, 'root/fungi-xoxo');
+ const layers = listByLayers(pipeline);
+ const columns = generateColumnsFromLayersListBare(pipeline, layers);
+
+ it('returns stage-like objects with default name, id, and status', () => {
+ columns.forEach((col, idx) => {
+ expect(col).toMatchObject({
+ name: '',
+ status: { action: null },
+ id: `layer-${idx}`,
+ });
+ });
+ });
+
+ it('creates groups that match the list created in listByLayers', () => {
+ columns.forEach((col, idx) => {
+ const groupNames = col.groups.map(({ name }) => name);
+ expect(groupNames).toEqual(layers[idx]);
+ });
+ });
+
+ it('looks up the correct group object', () => {
+ columns.forEach((col) => {
+ col.groups.forEach((group) => {
+ const groupStage = pipeline.stages.find((el) => el.name === group.stageName);
+ const groupObject = groupStage.groups.find((el) => el.name === group.name);
+ expect(group).toBe(groupObject);
+ });
+ });
+ });
+
+ /*
+ Just as a fallback in case multiple functions change, so tests pass
+ but the implementation moves away from case.
+ */
+ it('matches the snapshot', () => {
+ expect(columns).toMatchSnapshot();
+ });
+ });
});
diff --git a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
index 258f2bda829..7bac7036f46 100644
--- a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
+++ b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
@@ -1,18 +1,21 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { setHTMLFixture } from 'helpers/fixtures';
import { CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants';
import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
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 { DRAW_FAILURE } from '~/pipelines/constants';
-import { invalidNeedsData, pipelineData, singleStageData } from './mock_data';
+import { pipelineData, singleStageData } from './mock_data';
describe('pipeline graph component', () => {
const defaultProps = { pipelineData };
let wrapper;
+ const containerId = 'pipeline-graph-container-0';
+ setHTMLFixture(`<div id="${containerId}"></div>`);
+
const createComponent = (props = defaultProps) => {
return shallowMount(PipelineGraph, {
propsData: {
@@ -55,18 +58,7 @@ describe('pipeline graph component', () => {
it('renders the graph with no status error', () => {
expect(findAlert().exists()).toBe(false);
expect(findPipelineGraph().exists()).toBe(true);
- });
- });
-
- describe('with error while rendering the links with needs', () => {
- beforeEach(() => {
- wrapper = createComponent({ pipelineData: invalidNeedsData });
- });
-
- it('renders the error that link could not be drawn', () => {
expect(findLinksLayer().exists()).toBe(true);
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[DRAW_FAILURE]);
});
});
diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
new file mode 100644
index 00000000000..88b3ef2032a
--- /dev/null
+++ b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
@@ -0,0 +1,112 @@
+import { GlAlert, GlDropdown, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import PipelineMultiActions, {
+ i18n,
+} from '~/pipelines/components/pipelines_list/pipeline_multi_actions.vue';
+
+describe('Pipeline Multi Actions Dropdown', () => {
+ let wrapper;
+ let mockAxios;
+
+ const artifacts = [
+ {
+ name: 'job my-artifact',
+ path: '/download/path',
+ },
+ {
+ name: 'job-2 my-artifact-2',
+ path: '/download/path-two',
+ },
+ ];
+ const artifactItemTestId = 'artifact-item';
+ const artifactsEndpointPlaceholder = ':pipeline_artifacts_id';
+ const artifactsEndpoint = `endpoint/${artifactsEndpointPlaceholder}/artifacts.json`;
+ const pipelineId = 108;
+
+ const createComponent = ({ mockData = {} } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(PipelineMultiActions, {
+ provide: {
+ artifactsEndpoint,
+ artifactsEndpointPlaceholder,
+ },
+ propsData: {
+ pipelineId,
+ },
+ data() {
+ return {
+ ...mockData,
+ };
+ },
+ stubs: {
+ GlSprintf,
+ },
+ }),
+ );
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findAllArtifactItems = () => wrapper.findAllByTestId(artifactItemTestId);
+ const findFirstArtifactItem = () => wrapper.findByTestId(artifactItemTestId);
+
+ beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mockAxios.restore();
+
+ wrapper.destroy();
+ });
+
+ it('should render the dropdown', () => {
+ createComponent();
+
+ expect(findDropdown().exists()).toBe(true);
+ });
+
+ describe('Artifacts', () => {
+ it('should fetch artifacts on dropdown click', async () => {
+ const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
+ mockAxios.onGet(endpoint).replyOnce(200, { artifacts });
+ createComponent();
+ findDropdown().vm.$emit('show');
+ await waitForPromises();
+
+ expect(mockAxios.history.get).toHaveLength(1);
+ expect(wrapper.vm.artifacts).toEqual(artifacts);
+ });
+
+ it('should render all the provided artifacts', () => {
+ createComponent({ mockData: { artifacts } });
+
+ expect(findAllArtifactItems()).toHaveLength(artifacts.length);
+ });
+
+ it('should render the correct artifact name and path', () => {
+ createComponent({ mockData: { artifacts } });
+
+ expect(findFirstArtifactItem().attributes('href')).toBe(artifacts[0].path);
+ expect(findFirstArtifactItem().text()).toBe(`Download ${artifacts[0].name} artifact`);
+ });
+
+ describe('with a failing request', () => {
+ it('should render an error message', async () => {
+ const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
+ mockAxios.onGet(endpoint).replyOnce(500);
+ createComponent();
+ findDropdown().vm.$emit('show');
+ await waitForPromises();
+
+ const error = findAlert();
+ expect(error.exists()).toBe(true);
+ expect(error.text()).toBe(i18n.artifactsFetchErrorMessage);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js
index d4a2db08d97..336255768d7 100644
--- a/spec/frontend/pipelines/pipelines_artifacts_spec.js
+++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js
@@ -1,23 +1,43 @@
-import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlDropdown, GlDropdownItem, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import PipelineArtifacts, {
+ i18n,
+} from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
describe('Pipelines Artifacts dropdown', () => {
let wrapper;
+ let mockAxios;
- const createComponent = () => {
+ const artifacts = [
+ {
+ name: 'job my-artifact',
+ path: '/download/path',
+ },
+ {
+ name: 'job-2 my-artifact-2',
+ path: '/download/path-two',
+ },
+ ];
+ const artifactsEndpointPlaceholder = ':pipeline_artifacts_id';
+ const artifactsEndpoint = `endpoint/${artifactsEndpointPlaceholder}/artifacts.json`;
+ const pipelineId = 108;
+
+ const createComponent = ({ mockData = {} } = {}) => {
wrapper = shallowMount(PipelineArtifacts, {
+ provide: {
+ artifactsEndpoint,
+ artifactsEndpointPlaceholder,
+ },
propsData: {
- artifacts: [
- {
- name: 'job my-artifact',
- path: '/download/path',
- },
- {
- name: 'job-2 my-artifact-2',
- path: '/download/path-two',
- },
- ],
+ pipelineId,
+ },
+ data() {
+ return {
+ ...mockData,
+ };
},
stubs: {
GlSprintf,
@@ -25,11 +45,14 @@ describe('Pipelines Artifacts dropdown', () => {
});
};
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findFirstGlDropdownItem = () => wrapper.find(GlDropdownItem);
const findAllGlDropdownItems = () => wrapper.find(GlDropdown).findAll(GlDropdownItem);
beforeEach(() => {
- createComponent();
+ mockAxios = new MockAdapter(axios);
});
afterEach(() => {
@@ -37,13 +60,66 @@ describe('Pipelines Artifacts dropdown', () => {
wrapper = null;
});
+ it('should render the dropdown', () => {
+ createComponent();
+
+ expect(findDropdown().exists()).toBe(true);
+ });
+
+ it('should fetch artifacts on dropdown click', async () => {
+ const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
+ mockAxios.onGet(endpoint).replyOnce(200, { artifacts });
+ createComponent();
+ findDropdown().vm.$emit('show');
+ await waitForPromises();
+
+ expect(mockAxios.history.get).toHaveLength(1);
+ expect(wrapper.vm.artifacts).toEqual(artifacts);
+ });
+
it('should render a dropdown with all the provided artifacts', () => {
- expect(findAllGlDropdownItems()).toHaveLength(2);
+ createComponent({ mockData: { artifacts } });
+
+ expect(findAllGlDropdownItems()).toHaveLength(artifacts.length);
});
it('should render a link with the provided path', () => {
- expect(findFirstGlDropdownItem().attributes('href')).toBe('/download/path');
+ createComponent({ mockData: { artifacts } });
- expect(findFirstGlDropdownItem().text()).toBe('Download job my-artifact artifact');
+ expect(findFirstGlDropdownItem().attributes('href')).toBe(artifacts[0].path);
+
+ expect(findFirstGlDropdownItem().text()).toBe(`Download ${artifacts[0].name} artifact`);
+ });
+
+ describe('with a failing request', () => {
+ it('should render an error message', async () => {
+ const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
+ mockAxios.onGet(endpoint).replyOnce(500);
+ createComponent();
+ findDropdown().vm.$emit('show');
+ await waitForPromises();
+
+ const error = findAlert();
+ expect(error.exists()).toBe(true);
+ expect(error.text()).toBe(i18n.artifactsFetchErrorMessage);
+ });
+ });
+
+ describe('with no artifacts received', () => {
+ it('should render empty alert message', () => {
+ createComponent({ mockData: { artifacts: [] } });
+
+ const emptyAlert = findAlert();
+ expect(emptyAlert.exists()).toBe(true);
+ expect(emptyAlert.text()).toBe(i18n.noArtifacts);
+ });
+ });
+
+ describe('when artifacts are loading', () => {
+ it('should show loading icon', () => {
+ createComponent({ mockData: { isLoading: true } });
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/pipelines/pipelines_ci_templates_spec.js b/spec/frontend/pipelines/pipelines_ci_templates_spec.js
index d4cf6027ff7..0c37bf2d84a 100644
--- a/spec/frontend/pipelines/pipelines_ci_templates_spec.js
+++ b/spec/frontend/pipelines/pipelines_ci_templates_spec.js
@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import PipelinesCiTemplate from '~/pipelines/components/pipelines_list/pipelines_ci_templates.vue';
-const addCiYmlPath = "/-/new/master?commit_message='Add%20.gitlab-ci.yml'";
+const addCiYmlPath = "/-/new/main?commit_message='Add%20.gitlab-ci.yml'";
const suggestedCiTemplates = [
{ name: 'Android', logo: '/assets/illustrations/logos/android.svg' },
{ name: 'Bash', logo: '/assets/illustrations/logos/bash.svg' },
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index 84a25f42201..f9b59c5dc48 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -1,3 +1,4 @@
+import '~/commons';
import { GlButton, GlEmptyState, GlFilteredSearch, GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
@@ -6,6 +7,7 @@ import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
+import { getExperimentVariant } from '~/experimentation/utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
@@ -19,6 +21,10 @@ import TablePagination from '~/vue_shared/components/pagination/table_pagination
import { stageReply, users, mockSearch, branches } from './mock_data';
jest.mock('~/flash');
+jest.mock('~/experimentation/utils', () => ({
+ ...jest.requireActual('~/experimentation/utils'),
+ getExperimentVariant: jest.fn().mockReturnValue('control'),
+}));
const mockProjectPath = 'twitter/flight';
const mockProjectId = '21';
@@ -41,6 +47,7 @@ describe('Pipelines', () => {
ciLintPath: '/ci/lint',
resetCachePath: `${mockProjectPath}/settings/ci_cd/reset_cache`,
newPipelinePath: `${mockProjectPath}/pipelines/new`,
+ codeQualityPagePath: `${mockProjectPath}/-/new/master?commit_message=Add+.gitlab-ci.yml+and+create+a+code+quality+job&file_name=.gitlab-ci.yml&template=Code-Quality`,
};
const noPermissions = {
@@ -87,7 +94,10 @@ describe('Pipelines', () => {
beforeAll(() => {
origWindowLocation = window.location;
delete window.location;
- window.location = { search: '' };
+ window.location = {
+ search: '',
+ protocol: 'https:',
+ };
});
afterAll(() => {
@@ -289,7 +299,7 @@ describe('Pipelines', () => {
page: '1',
scope: 'all',
username: 'root',
- ref: 'master',
+ ref: 'main',
status: 'pending',
};
@@ -321,7 +331,7 @@ describe('Pipelines', () => {
expect(window.history.pushState).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
- `${window.location.pathname}?page=1&scope=all&username=root&ref=master&status=pending`,
+ `${window.location.pathname}?page=1&scope=all&username=root&ref=main&status=pending`,
);
});
});
@@ -551,6 +561,19 @@ describe('Pipelines', () => {
);
});
+ describe('when the code_quality_walkthrough experiment is active', () => {
+ beforeAll(() => {
+ getExperimentVariant.mockReturnValue('candidate');
+ });
+
+ it('renders another CTA button', () => {
+ expect(findEmptyState().findComponent(GlButton).text()).toBe('Add a code quality job');
+ expect(findEmptyState().findComponent(GlButton).attributes('href')).toBe(
+ paths.codeQualityPagePath,
+ );
+ });
+ });
+
it('does not render filtered search', () => {
expect(findFilteredSearch().exists()).toBe(false);
});
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
index 70e47b98575..68b0dfc018e 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -1,3 +1,4 @@
+import '~/commons';
import { GlTable } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -5,11 +6,11 @@ import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mi
import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue';
import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue';
import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue';
-import PipelinesStatusBadge from '~/pipelines/components/pipelines_list/pipelines_status_badge.vue';
import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import PipelinesTimeago from '~/pipelines/components/pipelines_list/time_ago.vue';
import eventHub from '~/pipelines/event_hub';
+import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import CommitComponent from '~/vue_shared/components/commit.vue';
jest.mock('~/pipelines/event_hub');
@@ -42,7 +43,7 @@ describe('Pipelines Table', () => {
};
const findGlTable = () => wrapper.findComponent(GlTable);
- const findStatusBadge = () => wrapper.findComponent(PipelinesStatusBadge);
+ const findStatusBadge = () => wrapper.findComponent(CiBadge);
const findPipelineInfo = () => wrapper.findComponent(PipelineUrl);
const findTriggerer = () => wrapper.findComponent(PipelineTriggerer);
const findCommit = () => wrapper.findComponent(CommitComponent);
diff --git a/spec/frontend/pipelines/test_reports/empty_state_spec.js b/spec/frontend/pipelines/test_reports/empty_state_spec.js
new file mode 100644
index 00000000000..ee0f8a90a11
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/empty_state_spec.js
@@ -0,0 +1,45 @@
+import { GlEmptyState } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import EmptyState, { i18n } from '~/pipelines/components/test_reports/empty_state.vue';
+
+describe('Test report empty state', () => {
+ let wrapper;
+
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ const createComponent = ({ hasTestReport = true } = {}) => {
+ wrapper = shallowMount(EmptyState, {
+ provide: {
+ emptyStateImagePath: '/image/path',
+ hasTestReport,
+ },
+ stubs: {
+ GlEmptyState,
+ },
+ });
+ };
+
+ describe('when pipeline has a test report', () => {
+ it('should render empty test report message', () => {
+ createComponent();
+
+ expect(findEmptyState().props()).toMatchObject({
+ primaryButtonText: i18n.noTestsButton,
+ description: i18n.noTestsDescription,
+ title: i18n.noTestsTitle,
+ });
+ });
+ });
+
+ describe('when pipeline does not have a test report', () => {
+ it('should render no test report message', () => {
+ createComponent({ hasTestReport: false });
+
+ expect(findEmptyState().props()).toMatchObject({
+ primaryButtonText: i18n.noReportsButton,
+ description: i18n.noReportsDescription,
+ title: i18n.noReportsTitle,
+ });
+ });
+ });
+});
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 e866586a2c3..c995eb864d1 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,6 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
@@ -13,29 +14,32 @@ describe('Test case details', () => {
formattedTime: '10.04ms',
recent_failures: {
count: 2,
- base_branch: 'master',
+ base_branch: 'main',
},
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 findModal = () => wrapper.findComponent(GlModal);
+ const findName = () => wrapper.findByTestId('test-case-name');
+ const findDuration = () => wrapper.findByTestId('test-case-duration');
+ const findRecentFailures = () => wrapper.findByTestId('test-case-recent-failures');
+ const findAttachmentUrl = () => wrapper.findByTestId('test-case-attachment-url');
+ const findSystemOutput = () => wrapper.findByTestId('test-case-trace');
const createComponent = (testCase = {}) => {
- wrapper = shallowMount(TestCaseDetails, {
- localVue,
- propsData: {
- modalId: 'my-modal',
- testCase: {
- ...defaultTestCase,
- ...testCase,
+ wrapper = extendedWrapper(
+ shallowMount(TestCaseDetails, {
+ localVue,
+ propsData: {
+ modalId: 'my-modal',
+ testCase: {
+ ...defaultTestCase,
+ ...testCase,
+ },
},
- },
- stubs: { CodeBlock, GlModal },
- });
+ stubs: { CodeBlock, GlModal },
+ }),
+ );
};
afterEach(() => {
@@ -91,6 +95,25 @@ describe('Test case details', () => {
});
});
+ describe('when test case has attachment URL', () => {
+ it('renders the attachment URL as a link', () => {
+ const expectedUrl = '/my/path.jpg';
+ createComponent({ attachment_url: expectedUrl });
+ const attachmentUrl = findAttachmentUrl();
+
+ expect(attachmentUrl.exists()).toBe(true);
+ expect(attachmentUrl.attributes('href')).toBe(expectedUrl);
+ });
+ });
+
+ describe('when test case does not have attachment URL', () => {
+ it('does not render the attachment URL', () => {
+ createComponent({ attachment_url: null });
+
+ expect(findAttachmentUrl().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 da5763ddf8e..e44d59ba888 100644
--- a/spec/frontend/pipelines/test_reports/test_reports_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js
@@ -2,6 +2,8 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { getJSONFixture } from 'helpers/fixtures';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import EmptyState from '~/pipelines/components/test_reports/empty_state.vue';
import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
import TestSummary from '~/pipelines/components/test_reports/test_summary.vue';
import TestSummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue';
@@ -16,11 +18,11 @@ describe('Test reports app', () => {
const testReports = getJSONFixture('pipelines/test_report.json');
- const loadingSpinner = () => wrapper.find(GlLoadingIcon);
- const testsDetail = () => wrapper.find('[data-testid="tests-detail"]');
- const noTestsToShow = () => wrapper.find('[data-testid="no-tests-to-show"]');
- const testSummary = () => wrapper.find(TestSummary);
- const testSummaryTable = () => wrapper.find(TestSummaryTable);
+ const loadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
+ const testsDetail = () => wrapper.findByTestId('tests-detail');
+ const emptyState = () => wrapper.findComponent(EmptyState);
+ const testSummary = () => wrapper.findComponent(TestSummary);
+ const testSummaryTable = () => wrapper.findComponent(TestSummaryTable);
const actionSpies = {
fetchTestSuite: jest.fn(),
@@ -29,7 +31,7 @@ describe('Test reports app', () => {
removeSelectedSuiteIndex: jest.fn(),
};
- const createComponent = (state = {}) => {
+ const createComponent = ({ state = {} } = {}) => {
store = new Vuex.Store({
state: {
isLoading: false,
@@ -41,10 +43,12 @@ describe('Test reports app', () => {
getters,
});
- wrapper = shallowMount(TestReports, {
- store,
- localVue,
- });
+ wrapper = extendedWrapper(
+ shallowMount(TestReports, {
+ store,
+ localVue,
+ }),
+ );
};
afterEach(() => {
@@ -52,33 +56,28 @@ describe('Test reports app', () => {
});
describe('when component is created', () => {
- beforeEach(() => {
+ it('should call fetchSummary when pipeline has test report', () => {
createComponent();
- });
- it('should call fetchSummary', () => {
expect(actionSpies.fetchSummary).toHaveBeenCalled();
});
});
describe('when loading', () => {
- beforeEach(() => createComponent({ isLoading: true }));
+ beforeEach(() => createComponent({ state: { isLoading: true } }));
it('shows the loading spinner', () => {
- expect(noTestsToShow().exists()).toBe(false);
+ expect(emptyState().exists()).toBe(false);
expect(testsDetail().exists()).toBe(false);
expect(loadingSpinner().exists()).toBe(true);
});
});
describe('when the api returns no data', () => {
- beforeEach(() => createComponent({ testReports: {} }));
-
- it('displays that there are no tests to show', () => {
- const noTests = noTestsToShow();
+ it('displays empty state component', () => {
+ createComponent({ state: { testReports: {} } });
- expect(noTests.exists()).toBe(true);
- expect(noTests.text()).toBe('There are no tests to show.');
+ expect(emptyState().exists()).toBe(true);
});
});
@@ -97,7 +96,7 @@ describe('Test reports app', () => {
describe('when a suite is clicked', () => {
beforeEach(() => {
- createComponent({ hasFullReport: true });
+ createComponent({ state: { hasFullReport: true } });
testSummaryTable().vm.$emit('row-click', 0);
});
@@ -109,7 +108,7 @@ describe('Test reports app', () => {
describe('when clicking back to summary', () => {
beforeEach(() => {
- createComponent({ selectedSuiteIndex: 0 });
+ createComponent({ state: { selectedSuiteIndex: 0 } });
testSummary().vm.$emit('on-back-click');
});
diff --git a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
index 2e32d62b4bd..2e44f40eda4 100644
--- a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
@@ -89,7 +89,7 @@ describe('Pipeline Branch Name Token', () => {
});
it('renders only the branch searched for', () => {
- const mockBranches = ['master'];
+ const mockBranches = ['main'];
createComponent({ stubs }, { branches: mockBranches, loading: false });
expect(findAllFilteredSearchSuggestions()).toHaveLength(mockBranches.length);
diff --git a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
index 42c9dfc9ff0..b03dbb73b95 100644
--- a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
@@ -89,7 +89,7 @@ describe('Pipeline Branch Name Token', () => {
});
it('renders only the tag searched for', () => {
- const mockTags = ['master-tag'];
+ const mockTags = ['main-tag'];
createComponent({ stubs }, { tags: mockTags, loading: false });
expect(findAllFilteredSearchSuggestions()).toHaveLength(mockTags.length);