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