summaryrefslogtreecommitdiff
path: root/spec/frontend/pipelines/graph
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/pipelines/graph')
-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
7 files changed, 384 insertions, 14 deletions
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',