diff options
Diffstat (limited to 'spec/frontend/pipelines/graph')
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', |