diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-18 09:45:46 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-18 09:45:46 +0000 |
commit | a7b3560714b4d9cc4ab32dffcd1f74a284b93580 (patch) | |
tree | 7452bd5c3545c2fa67a28aa013835fb4fa071baf /spec/frontend/pipelines | |
parent | ee9173579ae56a3dbfe5afe9f9410c65bb327ca7 (diff) | |
download | gitlab-ce-a7b3560714b4d9cc4ab32dffcd1f74a284b93580.tar.gz |
Add latest changes from gitlab-org/gitlab@14-8-stable-eev14.8.0-rc42
Diffstat (limited to 'spec/frontend/pipelines')
23 files changed, 1338 insertions, 353 deletions
diff --git a/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap b/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap index 52461885342..2d2e5db598a 100644 --- a/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap +++ b/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap @@ -30,6 +30,7 @@ Array [ "hasDetails": true, "icon": "status_success", "id": "7", + "label": "passed", "tooltip": "passed", }, }, @@ -71,6 +72,7 @@ Array [ "hasDetails": true, "icon": "status_success", "id": "12", + "label": "passed", "tooltip": "passed", }, }, @@ -112,6 +114,7 @@ Array [ "hasDetails": true, "icon": "status_success", "id": "17", + "label": "passed", "tooltip": "passed", }, }, @@ -153,6 +156,7 @@ Array [ "hasDetails": true, "icon": "status_success", "id": "22", + "label": "passed", "tooltip": "passed", }, }, @@ -178,6 +182,7 @@ Array [ "hasDetails": true, "icon": "status_success", "id": "25", + "label": "passed", "tooltip": "passed", }, }, @@ -203,6 +208,7 @@ Array [ "hasDetails": true, "icon": "status_success", "id": "28", + "label": "passed", "tooltip": "passed", }, }, @@ -237,6 +243,7 @@ Array [ "hasDetails": true, "icon": "status_success", "id": "60", + "label": null, "tooltip": null, }, }, @@ -295,6 +302,7 @@ Array [ "hasDetails": true, "icon": "status_success", "id": "35", + "label": "passed", "tooltip": "passed", }, }, @@ -348,6 +356,7 @@ Array [ "hasDetails": true, "icon": "status_success", "id": "43", + "label": "passed", "tooltip": "passed", }, }, @@ -385,6 +394,7 @@ Array [ "hasDetails": true, "icon": "status_success", "id": "50", + "label": "passed", "tooltip": "passed", }, }, @@ -423,6 +433,7 @@ Array [ "hasDetails": true, "icon": "status_success", "id": "64", + "label": null, "tooltip": null, }, }, diff --git a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js index 1941a7f2777..212f8e19a6d 100644 --- a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js +++ b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js @@ -1,5 +1,6 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount, mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import DagAnnotations from '~/pipelines/components/dag/dag_annotations.vue'; import { singleNote, multiNote } from './mock_data'; @@ -82,26 +83,24 @@ describe('The DAG annotations', () => { }); describe('clicking hide', () => { - it('hides listed items and changes text to show', () => { + it('hides listed items and changes text to show', async () => { expect(getAllTextBlocks().length).toBe(Object.keys(multiNote).length); expect(getToggleButton().text()).toBe('Hide list'); getToggleButton().trigger('click'); - return wrapper.vm.$nextTick().then(() => { - expect(getAllTextBlocks().length).toBe(0); - expect(getToggleButton().text()).toBe('Show list'); - }); + await nextTick(); + expect(getAllTextBlocks().length).toBe(0); + expect(getToggleButton().text()).toBe('Show list'); }); }); describe('clicking show', () => { - it('shows listed items and changes text to hide', () => { + it('shows listed items and changes text to hide', async () => { getToggleButton().trigger('click'); getToggleButton().trigger('click'); - return wrapper.vm.$nextTick().then(() => { - expect(getAllTextBlocks().length).toBe(Object.keys(multiNote).length); - expect(getToggleButton().text()).toBe('Hide list'); - }); + await nextTick(); + expect(getAllTextBlocks().length).toBe(Object.keys(multiNote).length); + expect(getToggleButton().text()).toBe('Hide list'); }); }); }); diff --git a/spec/frontend/pipelines/components/dag/dag_spec.js b/spec/frontend/pipelines/components/dag/dag_spec.js index 14030930657..d78df3eb35e 100644 --- a/spec/frontend/pipelines/components/dag/dag_spec.js +++ b/spec/frontend/pipelines/components/dag/dag_spec.js @@ -1,5 +1,6 @@ import { GlAlert, GlEmptyState } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from '~/pipelines/components/dag/constants'; import Dag from '~/pipelines/components/dag/dag.vue'; import DagAnnotations from '~/pipelines/components/dag/dag_annotations.vue'; @@ -153,11 +154,11 @@ describe('Pipeline DAG graph wrapper', () => { expect(getNotes().exists()).toBe(false); getGraph().vm.$emit('update-annotation', { type: ADD_NOTE, data: currentNote }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(getNotes().exists()).toBe(true); getGraph().vm.$emit('update-annotation', { type: REMOVE_NOTE, data: currentNote }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(getNotes().exists()).toBe(false); }); @@ -165,11 +166,11 @@ describe('Pipeline DAG graph wrapper', () => { expect(getNotes().exists()).toBe(false); getGraph().vm.$emit('update-annotation', { type: REPLACE_NOTES, data: multiNote }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(getNotes().exists()).toBe(true); getGraph().vm.$emit('update-annotation', { type: REPLACE_NOTES, data: {} }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(getNotes().exists()).toBe(false); }); }); diff --git a/spec/frontend/pipelines/components/jobs/jobs_app_spec.js b/spec/frontend/pipelines/components/jobs/jobs_app_spec.js index 1ea6096c922..65814ad9a7f 100644 --- a/spec/frontend/pipelines/components/jobs/jobs_app_spec.js +++ b/spec/frontend/pipelines/components/jobs/jobs_app_spec.js @@ -1,5 +1,6 @@ import { GlIntersectionObserver, GlSkeletonLoader } from '@gitlab/ui'; -import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; @@ -9,8 +10,7 @@ import JobsTable from '~/jobs/components/table/jobs_table.vue'; import getPipelineJobsQuery from '~/pipelines/graphql/queries/get_pipeline_jobs.query.graphql'; import { mockPipelineJobsQueryResponse } from '../../mock_data'; -const localVue = createLocalVue(); -localVue.use(VueApollo); +Vue.use(VueApollo); jest.mock('~/flash'); @@ -36,7 +36,6 @@ describe('Jobs app', () => { fullPath: 'root/ci-project', pipelineIid: 1, }, - localVue, apolloProvider: createMockApolloProvider(resolver), }); }; @@ -74,16 +73,16 @@ describe('Jobs app', () => { await waitForPromises(); expect(createFlash).toHaveBeenCalledWith({ - message: 'An error occured while fetching the pipelines jobs.', + message: 'An error occurred while fetching the pipelines jobs.', }); }); it('handles infinite scrolling by calling fetchMore', async () => { createComponent(resolverSpy); - await waitForPromises(); triggerInfiniteScroll(); + await waitForPromises(); expect(resolverSpy).toHaveBeenCalledWith({ after: 'eyJpZCI6Ijg0NyJ9', @@ -96,10 +95,10 @@ describe('Jobs app', () => { createComponent(resolverSpy); expect(findSkeletonLoader().exists()).toBe(true); - await waitForPromises(); triggerInfiniteScroll(); + await waitForPromises(); expect(findSkeletonLoader().exists()).toBe(false); }); diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js index 661c8d99477..97b59a09518 100644 --- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js +++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js @@ -1,6 +1,7 @@ import { GlFilteredSearch } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import Api from '~/api'; import axios from '~/lib/utils/axios_utils'; import PipelinesFilteredSearch from '~/pipelines/components/pipelines_list/pipelines_filtered_search.vue'; @@ -103,46 +104,42 @@ describe('Pipelines filtered search', () => { expect(wrapper.emitted('filterPipelines')[0]).toEqual([mockSearch]); }); - it('disables tag name token when branch name token is active', () => { + it('disables tag name token when branch name token is active', async () => { findFilteredSearch().vm.$emit('input', [ { type: 'ref', value: { data: 'branch-1', operator: '=' } }, { type: 'filtered-search-term', value: { data: '' } }, ]); - return wrapper.vm.$nextTick().then(() => { - expect(findBranchToken().disabled).toBe(false); - expect(findTagToken().disabled).toBe(true); - }); + await nextTick(); + expect(findBranchToken().disabled).toBe(false); + expect(findTagToken().disabled).toBe(true); }); - it('disables branch name token when tag name token is active', () => { + it('disables branch name token when tag name token is active', async () => { findFilteredSearch().vm.$emit('input', [ { type: 'tag', value: { data: 'tag-1', operator: '=' } }, { type: 'filtered-search-term', value: { data: '' } }, ]); - return wrapper.vm.$nextTick().then(() => { - expect(findBranchToken().disabled).toBe(true); - expect(findTagToken().disabled).toBe(false); - }); + await nextTick(); + expect(findBranchToken().disabled).toBe(true); + expect(findTagToken().disabled).toBe(false); }); - it('resets tokens disabled state on clear', () => { + it('resets tokens disabled state on clear', async () => { findFilteredSearch().vm.$emit('clearInput'); - return wrapper.vm.$nextTick().then(() => { - expect(findBranchToken().disabled).toBe(false); - expect(findTagToken().disabled).toBe(false); - }); + await nextTick(); + expect(findBranchToken().disabled).toBe(false); + expect(findTagToken().disabled).toBe(false); }); - it('resets tokens disabled state when clearing tokens by backspace', () => { + it('resets tokens disabled state when clearing tokens by backspace', async () => { findFilteredSearch().vm.$emit('input', [{ type: 'filtered-search-term', value: { data: '' } }]); - return wrapper.vm.$nextTick().then(() => { - expect(findBranchToken().disabled).toBe(false); - expect(findTagToken().disabled).toBe(false); - }); + await nextTick(); + expect(findBranchToken().disabled).toBe(false); + expect(findTagToken().disabled).toBe(false); }); describe('Url query params', () => { diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js index 177b026491c..fab6e6887b7 100644 --- a/spec/frontend/pipelines/graph/action_component_spec.js +++ b/spec/frontend/pipelines/graph/action_component_spec.js @@ -1,6 +1,7 @@ import { GlButton } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import axios from '~/lib/utils/axios_utils'; import ActionComponent from '~/pipelines/components/jobs_shared/action_component.vue'; @@ -9,6 +10,7 @@ describe('pipeline graph action component', () => { let wrapper; let mock; const findButton = () => wrapper.find(GlButton); + const findTooltipWrapper = () => wrapper.find('[data-testid="ci-action-icon-tooltip-wrapper"]'); beforeEach(() => { mock = new MockAdapter(axios); @@ -30,19 +32,14 @@ describe('pipeline graph action component', () => { }); it('should render the provided title as a bootstrap tooltip', () => { - expect(wrapper.attributes('title')).toBe('bar'); + expect(findTooltipWrapper().attributes('title')).toBe('bar'); }); - it('should update bootstrap tooltip when title changes', (done) => { + it('should update bootstrap tooltip when title changes', async () => { wrapper.setProps({ tooltipText: 'changed' }); - wrapper.vm - .$nextTick() - .then(() => { - expect(wrapper.attributes('title')).toBe('changed'); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(findTooltipWrapper().attributes('title')).toBe('changed'); }); it('should render an svg', () => { @@ -64,13 +61,11 @@ describe('pipeline graph action component', () => { .catch(done.fail); }); - it('renders a loading icon while waiting for request', (done) => { + it('renders a loading icon while waiting for request', async () => { findButton().trigger('click'); - wrapper.vm.$nextTick(() => { - expect(wrapper.find('.js-action-icon-loading').exists()).toBe(true); - done(); - }); + await nextTick(); + expect(wrapper.find('.js-action-icon-loading').exists()).toBe(true); }); }); }); diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js index 04e004dc6c1..8bc6c086b9d 100644 --- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js +++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js @@ -1,10 +1,11 @@ import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; -import Vue, { nextTick } from 'vue'; +import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; import getUserCallouts from '~/graphql_shared/queries/get_user_callouts.query.graphql'; import axios from '~/lib/utils/axios_utils'; @@ -100,15 +101,6 @@ describe('Pipeline graph wrapper', () => { wrapper.destroy(); }); - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterAll(() => { - jest.runOnlyPendingTimers(); - jest.useRealTimers(); - }); - describe('when data is loading', () => { it('displays the loading icon', () => { createComponentWithApollo(); @@ -134,8 +126,7 @@ describe('Pipeline graph wrapper', () => { describe('when data has loaded', () => { beforeEach(async () => { createComponentWithApollo(); - jest.runOnlyPendingTimers(); - await nextTick(); + await waitForPromises(); }); it('does not display the loading icon', () => { @@ -163,8 +154,7 @@ describe('Pipeline graph wrapper', () => { createComponentWithApollo({ getPipelineDetailsHandler: jest.fn().mockRejectedValue(new Error('GraphQL error')), }); - jest.runOnlyPendingTimers(); - await nextTick(); + await waitForPromises(); }); it('does not display the loading icon', () => { @@ -187,8 +177,7 @@ describe('Pipeline graph wrapper', () => { pipelineIid: '', }, }); - jest.runOnlyPendingTimers(); - await nextTick(); + await waitForPromises(); }); it('does not display the loading icon', () => { @@ -210,7 +199,7 @@ describe('Pipeline graph wrapper', () => { createComponentWithApollo(); jest.spyOn(wrapper.vm.$apollo.queries.headerPipeline, 'refetch'); jest.spyOn(wrapper.vm.$apollo.queries.pipeline, 'refetch'); - await nextTick(); + await waitForPromises(); getGraph().vm.$emit('refreshPipelineGraph'); }); @@ -224,8 +213,7 @@ describe('Pipeline graph wrapper', () => { describe('when query times out', () => { const advanceApolloTimers = async () => { jest.runOnlyPendingTimers(); - await nextTick(); - await nextTick(); + await waitForPromises(); }; beforeEach(async () => { @@ -245,7 +233,7 @@ describe('Pipeline graph wrapper', () => { .mockResolvedValueOnce(errorData); createComponentWithApollo({ getPipelineDetailsHandler: failSucceedFail }); - await nextTick(); + await waitForPromises(); }); it('shows correct errors and does not overwrite populated data when data is empty', async () => { @@ -274,8 +262,7 @@ describe('Pipeline graph wrapper', () => { mountFn: mount, }); - jest.runOnlyPendingTimers(); - await nextTick(); + await waitForPromises(); }); it('appears when pipeline uses needs', () => { @@ -318,7 +305,7 @@ describe('Pipeline graph wrapper', () => { }); jest.runOnlyPendingTimers(); - await nextTick(); + await waitForPromises(); }); it('sets showLinks to true', async () => { @@ -327,8 +314,9 @@ describe('Pipeline graph wrapper', () => { expect(getLinksLayer().props('showLinks')).toBe(false); expect(getViewSelector().props('type')).toBe(LAYER_VIEW); await getDependenciesToggle().vm.$emit('change', true); + jest.runOnlyPendingTimers(); - await nextTick(); + await waitForPromises(); expect(wrapper.findComponent(LinksLayer).props('showLinks')).toBe(true); }); }); @@ -343,8 +331,7 @@ describe('Pipeline graph wrapper', () => { mountFn: mount, }); - jest.runOnlyPendingTimers(); - await nextTick(); + await waitForPromises(); }); it('shows the hover tip in the view selector', async () => { @@ -365,7 +352,7 @@ describe('Pipeline graph wrapper', () => { }); jest.runOnlyPendingTimers(); - await nextTick(); + await waitForPromises(); }); it('does not show the hover tip', async () => { @@ -382,8 +369,7 @@ describe('Pipeline graph wrapper', () => { mountFn: mount, }); - jest.runOnlyPendingTimers(); - await nextTick(); + await waitForPromises(); }); afterEach(() => { @@ -411,8 +397,7 @@ describe('Pipeline graph wrapper', () => { getPipelineDetailsHandler: jest.fn().mockResolvedValue(nonNeedsResponse), }); - jest.runOnlyPendingTimers(); - await nextTick(); + await waitForPromises(); }); afterEach(() => { @@ -435,7 +420,7 @@ describe('Pipeline graph wrapper', () => { }); jest.runOnlyPendingTimers(); - await nextTick(); + await waitForPromises(); }); it('does not appear when pipeline does not use needs', () => { @@ -461,8 +446,7 @@ describe('Pipeline graph wrapper', () => { describe('with no metrics path', () => { beforeEach(async () => { createComponentWithApollo(); - jest.runOnlyPendingTimers(); - await nextTick(); + await waitForPromises(); }); it('is not called', () => { @@ -505,8 +489,7 @@ describe('Pipeline graph wrapper', () => { }, }); - jest.runOnlyPendingTimers(); - await nextTick(); + await waitForPromises(); }); it('attempts to collect metrics', () => { @@ -517,7 +500,7 @@ describe('Pipeline graph wrapper', () => { }); describe('with duration and no error', () => { - beforeEach(() => { + beforeEach(async () => { mock = new MockAdapter(axios); mock.onPost(metricsPath).reply(200, {}); @@ -536,6 +519,7 @@ describe('Pipeline graph wrapper', () => { currentViewType: LAYER_VIEW, }, }); + await waitForPromises(); }); afterEach(() => { diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js index 06f1fa4c827..23e7ed7ebb4 100644 --- a/spec/frontend/pipelines/graph/job_item_spec.js +++ b/spec/frontend/pipelines/graph/job_item_spec.js @@ -1,4 +1,5 @@ import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import JobItem from '~/pipelines/components/graph/job_item.vue'; describe('pipeline graph job item', () => { @@ -6,6 +7,7 @@ describe('pipeline graph job item', () => { const findJobWithoutLink = () => wrapper.find('[data-testid="job-without-link"]'); const findJobWithLink = () => wrapper.find('[data-testid="job-with-link"]'); + const findActionComponent = () => wrapper.find('[data-testid="ci-action-component"]'); const createWrapper = (propsData) => { wrapper = mount(JobItem, { @@ -68,28 +70,38 @@ describe('pipeline graph job item', () => { hasDetails: false, }, }; + const mockJobWithUnauthorizedAction = { + id: 4258, + name: 'stop-environment', + status: { + icon: 'status_manual', + label: 'manual stop action (not allowed)', + tooltip: 'manual action', + group: 'manual', + detailsPath: '/root/ci-mock/builds/4258', + hasDetails: true, + action: null, + }, + }; afterEach(() => { wrapper.destroy(); }); describe('name with link', () => { - it('should render the job name and status with a link', (done) => { + it('should render the job name and status with a link', async () => { createWrapper({ job: mockJob }); - wrapper.vm.$nextTick(() => { - const link = wrapper.find('a'); - - expect(link.attributes('href')).toBe(mockJob.status.detailsPath); + await nextTick(); + const link = wrapper.find('a'); - expect(link.attributes('title')).toBe(`${mockJob.name} - ${mockJob.status.label}`); + expect(link.attributes('href')).toBe(mockJob.status.detailsPath); - expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); + expect(link.attributes('title')).toBe(`${mockJob.name} - ${mockJob.status.label}`); - expect(wrapper.text()).toBe(mockJob.name); + expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); - done(); - }); + expect(wrapper.text()).toBe(mockJob.name); }); }); @@ -118,8 +130,21 @@ describe('pipeline graph job item', () => { it('it should render the action icon', () => { createWrapper({ job: mockJob }); - expect(wrapper.find('.ci-action-icon-container').exists()).toBe(true); - expect(wrapper.find('.ci-action-icon-wrapper').exists()).toBe(true); + const actionComponent = findActionComponent(); + + expect(actionComponent.exists()).toBe(true); + expect(actionComponent.props('actionIcon')).toBe('retry'); + expect(actionComponent.attributes('disabled')).not.toBe('disabled'); + }); + + it('it should render disabled action icon when user cannot run the action', () => { + createWrapper({ job: mockJobWithUnauthorizedAction }); + + const actionComponent = findActionComponent(); + + expect(actionComponent.exists()).toBe(true); + expect(actionComponent.props('actionIcon')).toBe('stop'); + expect(actionComponent.attributes('disabled')).toBe('disabled'); }); }); diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js index af5cd907dd8..d800a8c341e 100644 --- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js +++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js @@ -9,6 +9,23 @@ import mockPipeline from './linked_pipelines_mock_data'; describe('Linked pipeline', () => { let wrapper; + const downstreamProps = { + pipeline: { + ...mockPipeline, + multiproject: false, + }, + columnTitle: 'Downstream', + type: DOWNSTREAM, + expanded: false, + isLoading: false, + }; + + const upstreamProps = { + ...downstreamProps, + columnTitle: 'Upstream', + type: UPSTREAM, + }; + const findButton = () => wrapper.find(GlButton); const findDownstreamPipelineTitle = () => wrapper.find('[data-testid="downstream-title"]'); const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]'); @@ -86,91 +103,65 @@ describe('Linked pipeline', () => { }); }); - describe('parent/child', () => { - const downstreamProps = { - pipeline: { - ...mockPipeline, - multiproject: false, - }, - columnTitle: 'Downstream', - type: DOWNSTREAM, - expanded: false, - isLoading: false, - }; + describe('upstream pipelines', () => { + beforeEach(() => { + createWrapper(upstreamProps); + }); - const upstreamProps = { - ...downstreamProps, - columnTitle: 'Upstream', - type: UPSTREAM, - }; + it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => { + expect(findPipelineLabel().exists()).toBe(true); + }); - it('parent/child label container should exist', () => { + it('upstream pipeline should contain the correct link', () => { + expect(findPipelineLink().attributes('href')).toBe(upstreamProps.pipeline.path); + }); + + it('applies the reverse-row css class to the card', () => { + expect(findLinkedPipeline().classes()).toContain('gl-flex-direction-row-reverse'); + expect(findLinkedPipeline().classes()).not.toContain('gl-flex-direction-row'); + }); + }); + + describe('downstream pipelines', () => { + beforeEach(() => { createWrapper(downstreamProps); + }); + + it('parent/child label container should exist', () => { expect(findPipelineLabel().exists()).toBe(true); }); it('should display child label when pipeline project id is the same as triggered pipeline project id', () => { - createWrapper(downstreamProps); expect(findPipelineLabel().exists()).toBe(true); }); it('should have the name of the trigger job on the card when it is a child pipeline', () => { - createWrapper(downstreamProps); expect(findDownstreamPipelineTitle().text()).toBe(mockPipeline.sourceJob.name); }); - it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => { - createWrapper(upstreamProps); - expect(findPipelineLabel().exists()).toBe(true); - }); - it('downstream pipeline should contain the correct link', () => { - createWrapper(downstreamProps); expect(findPipelineLink().attributes('href')).toBe(downstreamProps.pipeline.path); }); - it('upstream pipeline should contain the correct link', () => { - createWrapper(upstreamProps); - expect(findPipelineLink().attributes('href')).toBe(upstreamProps.pipeline.path); + it('applies the flex-row css class to the card', () => { + expect(findLinkedPipeline().classes()).toContain('gl-flex-direction-row'); + expect(findLinkedPipeline().classes()).not.toContain('gl-flex-direction-row-reverse'); }); + }); + describe('expand button', () => { it.each` - presentClass | missingClass - ${'gl-right-0'} | ${'gl-left-0'} - ${'gl-border-l-1!'} | ${'gl-border-r-1!'} - `( - 'pipeline expand button should be postioned right when child pipeline', - ({ presentClass, missingClass }) => { - createWrapper(downstreamProps); - expect(findExpandButton().classes()).toContain(presentClass); - expect(findExpandButton().classes()).not.toContain(missingClass); - }, - ); - - it.each` - presentClass | missingClass - ${'gl-left-0'} | ${'gl-right-0'} - ${'gl-border-r-1!'} | ${'gl-border-l-1!'} - `( - 'pipeline expand button should be postioned left when parent pipeline', - ({ presentClass, missingClass }) => { - createWrapper(upstreamProps); - expect(findExpandButton().classes()).toContain(presentClass); - expect(findExpandButton().classes()).not.toContain(missingClass); - }, - ); - - it.each` - pipelineType | anglePosition | expanded - ${downstreamProps} | ${'angle-right'} | ${false} - ${downstreamProps} | ${'angle-left'} | ${true} - ${upstreamProps} | ${'angle-left'} | ${false} - ${upstreamProps} | ${'angle-right'} | ${true} + pipelineType | anglePosition | borderClass | expanded + ${downstreamProps} | ${'angle-right'} | ${'gl-border-l-1!'} | ${false} + ${downstreamProps} | ${'angle-left'} | ${'gl-border-l-1!'} | ${true} + ${upstreamProps} | ${'angle-left'} | ${'gl-border-r-1!'} | ${false} + ${upstreamProps} | ${'angle-right'} | ${'gl-border-r-1!'} | ${true} `( - '$pipelineType.columnTitle pipeline button icon should be $anglePosition if expanded state is $expanded', - ({ pipelineType, anglePosition, expanded }) => { + '$pipelineType.columnTitle pipeline button icon should be $anglePosition with $borderClass if expanded state is $expanded', + ({ pipelineType, anglePosition, borderClass, expanded }) => { createWrapper({ ...pipelineType, expanded }); expect(findExpandButton().props('icon')).toBe(anglePosition); + expect(findExpandButton().classes()).toContain(borderClass); }, ); }); diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js index 2f03b846525..1673065e09c 100644 --- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js +++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js @@ -1,6 +1,8 @@ -import { mount, shallowMount, createLocalVue } from '@vue/test-utils'; +import { mount, shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; import { DOWNSTREAM, @@ -40,13 +42,11 @@ describe('Linked Pipelines Column', () => { const findPipelineGraph = () => wrapper.find(PipelineGraph); const findExpandButton = () => wrapper.find('[data-testid="expand-pipeline-button"]'); - const localVue = createLocalVue(); - localVue.use(VueApollo); + Vue.use(VueApollo); const createComponent = ({ apolloProvider, mountFn = shallowMount, props = {} } = {}) => { wrapper = mountFn(LinkedPipelinesColumn, { apolloProvider, - localVue, propsData: { ...defaultProps, ...props, @@ -87,13 +87,7 @@ describe('Linked Pipelines Column', () => { describe('click action', () => { const clickExpandButton = async () => { await findExpandButton().trigger('click'); - await wrapper.vm.$nextTick(); - }; - - const clickExpandButtonAndAwaitTimers = async () => { - await clickExpandButton(); - jest.runOnlyPendingTimers(); - await wrapper.vm.$nextTick(); + await waitForPromises(); }; describe('layer type rendering', () => { @@ -106,9 +100,9 @@ describe('Linked Pipelines Column', () => { it('calls listByLayers only once no matter how many times view is switched', async () => { expect(layersFn).not.toHaveBeenCalled(); - await clickExpandButtonAndAwaitTimers(); + await clickExpandButton(); await wrapper.setProps({ viewType: LAYER_VIEW }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(layersFn).toHaveBeenCalledTimes(1); await wrapper.setProps({ viewType: STAGE_VIEW }); await wrapper.setProps({ viewType: LAYER_VIEW }); @@ -132,7 +126,7 @@ describe('Linked Pipelines Column', () => { }); it('shows the stage view, even when the main graph view type is layers', async () => { - await clickExpandButtonAndAwaitTimers(); + await clickExpandButton(); expect(findPipelineGraph().props('viewType')).toBe(STAGE_VIEW); }); }); @@ -145,7 +139,7 @@ describe('Linked Pipelines Column', () => { it('toggles the pipeline visibility', async () => { expect(findPipelineGraph().exists()).toBe(false); - await clickExpandButtonAndAwaitTimers(); + await clickExpandButton(); expect(findPipelineGraph().exists()).toBe(true); await clickExpandButton(); expect(findPipelineGraph().exists()).toBe(false); @@ -167,7 +161,7 @@ describe('Linked Pipelines Column', () => { it('does not show the pipeline', async () => { expect(findPipelineGraph().exists()).toBe(false); - await clickExpandButtonAndAwaitTimers(); + await clickExpandButton(); expect(findPipelineGraph().exists()).toBe(false); }); }); @@ -195,7 +189,7 @@ describe('Linked Pipelines Column', () => { it('toggles the pipeline visibility', async () => { expect(findPipelineGraph().exists()).toBe(false); - await clickExpandButtonAndAwaitTimers(); + await clickExpandButton(); expect(findPipelineGraph().exists()).toBe(true); await clickExpandButton(); expect(findPipelineGraph().exists()).toBe(false); @@ -218,7 +212,7 @@ describe('Linked Pipelines Column', () => { it('does not show the pipeline', async () => { expect(findPipelineGraph().exists()).toBe(false); - await clickExpandButtonAndAwaitTimers(); + await clickExpandButton(); expect(findPipelineGraph().exists()).toBe(false); }); }); diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js index 41823bfdb9f..0cf7dc507f4 100644 --- a/spec/frontend/pipelines/graph/mock_data.js +++ b/spec/frontend/pipelines/graph/mock_data.js @@ -57,6 +57,7 @@ export const mockPipelineResponse = { id: '7', icon: 'status_success', tooltip: 'passed', + label: 'passed', hasDetails: true, detailsPath: '/root/abcd-dag/-/jobs/1482', group: 'success', @@ -106,6 +107,7 @@ export const mockPipelineResponse = { id: '12', icon: 'status_success', tooltip: 'passed', + label: 'passed', hasDetails: true, detailsPath: '/root/abcd-dag/-/jobs/1515', group: 'success', @@ -155,6 +157,7 @@ export const mockPipelineResponse = { id: '17', icon: 'status_success', tooltip: 'passed', + label: 'passed', hasDetails: true, detailsPath: '/root/abcd-dag/-/jobs/1484', group: 'success', @@ -204,6 +207,7 @@ export const mockPipelineResponse = { id: '22', icon: 'status_success', tooltip: 'passed', + label: 'passed', hasDetails: true, detailsPath: '/root/abcd-dag/-/jobs/1485', group: 'success', @@ -235,6 +239,7 @@ export const mockPipelineResponse = { id: '25', icon: 'status_success', tooltip: 'passed', + label: 'passed', hasDetails: true, detailsPath: '/root/abcd-dag/-/jobs/1486', group: 'success', @@ -266,6 +271,7 @@ export const mockPipelineResponse = { id: '28', icon: 'status_success', tooltip: 'passed', + label: 'passed', hasDetails: true, detailsPath: '/root/abcd-dag/-/jobs/1487', group: 'success', @@ -330,6 +336,7 @@ export const mockPipelineResponse = { id: '35', icon: 'status_success', tooltip: 'passed', + label: 'passed', hasDetails: true, detailsPath: '/root/abcd-dag/-/jobs/1514', group: 'success', @@ -413,6 +420,7 @@ export const mockPipelineResponse = { id: '43', icon: 'status_success', tooltip: 'passed', + label: 'passed', hasDetails: true, detailsPath: '/root/abcd-dag/-/jobs/1489', group: 'success', @@ -498,6 +506,7 @@ export const mockPipelineResponse = { id: '50', icon: 'status_success', tooltip: 'passed', + label: 'passed', hasDetails: true, detailsPath: '/root/abcd-dag/-/jobs/1490', group: 'success', @@ -601,6 +610,7 @@ export const mockPipelineResponse = { id: '60', icon: 'status_success', tooltip: null, + label: null, hasDetails: true, detailsPath: '/root/kinder-pipe/-/pipelines/154', group: 'success', @@ -643,6 +653,7 @@ export const mockPipelineResponse = { id: '64', icon: 'status_success', tooltip: null, + label: null, hasDetails: true, detailsPath: '/root/abcd-dag/-/pipelines/153', group: 'success', @@ -850,6 +861,7 @@ export const wrappedPipelineReturn = { id: '84', icon: 'status_success', tooltip: 'passed', + label: 'passed', hasDetails: true, detailsPath: '/root/elemenohpee/-/jobs/1662', group: 'success', diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js index 9e51003da66..1d89f949564 100644 --- a/spec/frontend/pipelines/header_component_spec.js +++ b/spec/frontend/pipelines/header_component_spec.js @@ -4,6 +4,7 @@ import HeaderComponent from '~/pipelines/components/header_component.vue'; import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql'; import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql'; import retryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql'; +import { BUTTON_TOOLTIP_RETRY } from '~/pipelines/constants'; import { mockCancelledPipelineHeader, mockFailedPipelineHeader, @@ -113,6 +114,10 @@ describe('Pipeline details header', () => { variables: { id: mockCancelledPipelineHeader.id }, }); }); + + it('should render retry action tooltip', () => { + expect(findRetryButton().attributes('title')).toBe(BUTTON_TOOLTIP_RETRY); + }); }); describe('Cancel action', () => { diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js index b9d20eb7ca5..8cb6cf3bed6 100644 --- a/spec/frontend/pipelines/mock_data.js +++ b/spec/frontend/pipelines/mock_data.js @@ -634,3 +634,683 @@ export const mockPipelineJobsQueryResponse = { }, }, }; + +export const mockPipeline = (projectPath) => { + return { + pipeline: { + id: 1, + user: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: '', + web_url: 'http://0.0.0.0:3000/root', + show_status: false, + path: '/root', + }, + active: false, + source: 'merge_request_event', + created_at: '2021-10-19T21:17:38.698Z', + updated_at: '2021-10-21T18:00:42.758Z', + path: 'foo', + flags: {}, + merge_request: { + iid: 1, + path: `/${projectPath}/1`, + title: 'commit', + source_branch: 'test-commit-name', + source_branch_path: `/${projectPath}`, + target_branch: 'main', + target_branch_path: `/${projectPath}/-/commit/main`, + }, + ref: { + name: 'refs/merge-requests/1/head', + path: `/${projectPath}/-/commits/refs/merge-requests/1/head`, + tag: false, + branch: false, + merge_request: true, + }, + commit: { + id: 'fd6df5b3229e213c97d308844a6f3e7fd71e8f8c', + short_id: 'fd6df5b3', + created_at: '2021-10-19T21:17:12.000+00:00', + parent_ids: ['7147906b84306e83cb3fec6582a25390b75713c6'], + title: 'Commit Title', + message: 'Commit', + author_name: 'Administrator', + author_email: 'admin@example.com', + authored_date: '2021-10-19T21:17:12.000+00:00', + committer_name: 'Administrator', + committer_email: 'admin@example.com', + committed_date: '2021-10-19T21:17:12.000+00:00', + trailers: {}, + web_url: '', + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: '', + web_url: '', + show_status: false, + path: '/root', + }, + author_gravatar_url: '', + commit_url: `/${projectPath}/fd6df5b3229e213c97d308844a6f3e7fd71e8f8c`, + commit_path: `/${projectPath}/commit/fd6df5b3229e213c97d308844a6f3e7fd71e8f8c`, + }, + project: { + full_path: `/${projectPath}`, + }, + triggered_by: null, + triggered: [], + }, + pipelineScheduleUrl: 'foo', + pipelineKey: 'id', + viewType: 'root', + }; +}; + +export const mockPipelineTag = () => { + return { + pipeline: { + id: 311, + iid: 37, + user: { + id: 1, + username: 'root', + name: 'Administrator', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'http://gdk.test:3000/root', + show_status: false, + path: '/root', + }, + active: false, + source: 'push', + created_at: '2022-02-02T15:39:04.012Z', + updated_at: '2022-02-02T15:40:59.573Z', + path: '/root/mr-widgets/-/pipelines/311', + flags: { + stuck: false, + auto_devops: false, + merge_request: false, + yaml_errors: false, + retryable: true, + cancelable: false, + failure_reason: false, + detached_merge_request_pipeline: false, + merge_request_pipeline: false, + merge_train_pipeline: false, + latest: true, + }, + details: { + status: { + icon: 'status_warning', + text: 'passed', + label: 'passed with warnings', + group: 'success-with-warnings', + tooltip: 'passed', + has_details: true, + details_path: '/root/mr-widgets/-/pipelines/311', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + stages: [ + { + name: 'accessibility', + title: 'accessibility: passed', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/root/mr-widgets/-/pipelines/311#accessibility', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + path: '/root/mr-widgets/-/pipelines/311#accessibility', + dropdown_path: '/root/mr-widgets/-/pipelines/311/stage.json?stage=accessibility', + }, + { + name: 'validate', + title: 'validate: passed with warnings', + status: { + icon: 'status_warning', + text: 'passed', + label: 'passed with warnings', + group: 'success-with-warnings', + tooltip: 'passed', + has_details: true, + details_path: '/root/mr-widgets/-/pipelines/311#validate', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + path: '/root/mr-widgets/-/pipelines/311#validate', + dropdown_path: '/root/mr-widgets/-/pipelines/311/stage.json?stage=validate', + }, + { + name: 'test', + title: 'test: passed', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/root/mr-widgets/-/pipelines/311#test', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + path: '/root/mr-widgets/-/pipelines/311#test', + dropdown_path: '/root/mr-widgets/-/pipelines/311/stage.json?stage=test', + }, + { + name: 'build', + title: 'build: passed', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/root/mr-widgets/-/pipelines/311#build', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + path: '/root/mr-widgets/-/pipelines/311#build', + dropdown_path: '/root/mr-widgets/-/pipelines/311/stage.json?stage=build', + }, + ], + duration: 93, + finished_at: '2022-02-02T15:40:59.384Z', + name: 'Pipeline', + manual_actions: [], + scheduled_actions: [], + }, + ref: { + name: 'test', + path: '/root/mr-widgets/-/commits/test', + tag: true, + branch: false, + merge_request: false, + }, + commit: { + id: '9b92b4f730d1611bd9a086ca221ae206d5da1e59', + short_id: '9b92b4f7', + created_at: '2022-01-13T13:59:03.000+00:00', + parent_ids: ['0ba763634114e207dc72c65c8e9459556b1204fb'], + title: 'Update hello_world.js', + message: 'Update hello_world.js', + author_name: 'Administrator', + author_email: 'admin@example.com', + authored_date: '2022-01-13T13:59:03.000+00:00', + committer_name: 'Administrator', + committer_email: 'admin@example.com', + committed_date: '2022-01-13T13:59:03.000+00:00', + trailers: {}, + web_url: + 'http://gdk.test:3000/root/mr-widgets/-/commit/9b92b4f730d1611bd9a086ca221ae206d5da1e59', + author: { + id: 1, + username: 'root', + name: 'Administrator', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'http://gdk.test:3000/root', + show_status: false, + path: '/root', + }, + author_gravatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + commit_url: + 'http://gdk.test:3000/root/mr-widgets/-/commit/9b92b4f730d1611bd9a086ca221ae206d5da1e59', + commit_path: '/root/mr-widgets/-/commit/9b92b4f730d1611bd9a086ca221ae206d5da1e59', + }, + retry_path: '/root/mr-widgets/-/pipelines/311/retry', + delete_path: '/root/mr-widgets/-/pipelines/311', + failed_builds: [ + { + id: 1696, + name: 'fmt', + started: '2022-02-02T15:39:45.192Z', + complete: true, + archived: false, + build_path: '/root/mr-widgets/-/jobs/1696', + retry_path: '/root/mr-widgets/-/jobs/1696/retry', + playable: false, + scheduled: false, + created_at: '2022-02-02T15:39:04.136Z', + updated_at: '2022-02-02T15:39:57.969Z', + status: { + icon: 'status_warning', + text: 'failed', + label: 'failed (allowed to fail)', + group: 'failed-with-warnings', + tooltip: 'failed - (script failure) (allowed to fail)', + has_details: true, + details_path: '/root/mr-widgets/-/jobs/1696', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/mr-widgets/-/jobs/1696/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + recoverable: false, + }, + ], + project: { + id: 23, + name: 'mr-widgets', + full_path: '/root/mr-widgets', + full_name: 'Administrator / mr-widgets', + }, + triggered_by: null, + triggered: [], + }, + pipelineScheduleUrl: 'foo', + pipelineKey: 'id', + viewType: 'root', + }; +}; + +export const mockPipelineBranch = () => { + return { + pipeline: { + id: 268, + iid: 34, + user: { + id: 1, + username: 'root', + name: 'Administrator', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'http://gdk.test:3000/root', + show_status: false, + path: '/root', + }, + active: false, + source: 'push', + created_at: '2022-01-14T17:40:27.866Z', + updated_at: '2022-01-14T18:02:35.850Z', + path: '/root/mr-widgets/-/pipelines/268', + flags: { + stuck: false, + auto_devops: false, + merge_request: false, + yaml_errors: false, + retryable: true, + cancelable: false, + failure_reason: false, + detached_merge_request_pipeline: false, + merge_request_pipeline: false, + merge_train_pipeline: false, + latest: true, + }, + details: { + status: { + icon: 'status_warning', + text: 'passed', + label: 'passed with warnings', + group: 'success-with-warnings', + tooltip: 'passed', + has_details: true, + details_path: '/root/mr-widgets/-/pipelines/268', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + stages: [ + { + name: 'validate', + title: 'validate: passed with warnings', + status: { + icon: 'status_warning', + text: 'passed', + label: 'passed with warnings', + group: 'success-with-warnings', + tooltip: 'passed', + has_details: true, + details_path: '/root/mr-widgets/-/pipelines/268#validate', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + path: '/root/mr-widgets/-/pipelines/268#validate', + dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=validate', + }, + { + name: 'test', + title: 'test: passed', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/root/mr-widgets/-/pipelines/268#test', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + path: '/root/mr-widgets/-/pipelines/268#test', + dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=test', + }, + { + name: 'build', + title: 'build: passed', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/root/mr-widgets/-/pipelines/268#build', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + path: '/root/mr-widgets/-/pipelines/268#build', + dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=build', + }, + ], + duration: 75, + finished_at: '2022-01-14T18:02:35.842Z', + name: 'Pipeline', + manual_actions: [], + scheduled_actions: [], + }, + ref: { + name: 'update-ci', + path: '/root/mr-widgets/-/commits/update-ci', + tag: false, + branch: true, + merge_request: false, + }, + commit: { + id: '96aef9ecec5752c09371c1ade5fc77860aafc863', + short_id: '96aef9ec', + created_at: '2022-01-14T17:40:26.000+00:00', + parent_ids: ['06860257572d4cf84b73806250b78169050aed83'], + title: 'Update main.tf', + message: 'Update main.tf', + author_name: 'Administrator', + author_email: 'admin@example.com', + authored_date: '2022-01-14T17:40:26.000+00:00', + committer_name: 'Administrator', + committer_email: 'admin@example.com', + committed_date: '2022-01-14T17:40:26.000+00:00', + trailers: {}, + web_url: + 'http://gdk.test:3000/root/mr-widgets/-/commit/96aef9ecec5752c09371c1ade5fc77860aafc863', + author: { + id: 1, + username: 'root', + name: 'Administrator', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'http://gdk.test:3000/root', + show_status: false, + path: '/root', + }, + author_gravatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + commit_url: + 'http://gdk.test:3000/root/mr-widgets/-/commit/96aef9ecec5752c09371c1ade5fc77860aafc863', + commit_path: '/root/mr-widgets/-/commit/96aef9ecec5752c09371c1ade5fc77860aafc863', + }, + retry_path: '/root/mr-widgets/-/pipelines/268/retry', + delete_path: '/root/mr-widgets/-/pipelines/268', + failed_builds: [ + { + id: 1260, + name: 'fmt', + started: '2022-01-14T17:40:36.435Z', + complete: true, + archived: false, + build_path: '/root/mr-widgets/-/jobs/1260', + retry_path: '/root/mr-widgets/-/jobs/1260/retry', + playable: false, + scheduled: false, + created_at: '2022-01-14T17:40:27.879Z', + updated_at: '2022-01-14T17:40:42.129Z', + status: { + icon: 'status_warning', + text: 'failed', + label: 'failed (allowed to fail)', + group: 'failed-with-warnings', + tooltip: 'failed - (script failure) (allowed to fail)', + has_details: true, + details_path: '/root/mr-widgets/-/jobs/1260', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/mr-widgets/-/jobs/1260/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + recoverable: false, + }, + ], + project: { + id: 23, + name: 'mr-widgets', + full_path: '/root/mr-widgets', + full_name: 'Administrator / mr-widgets', + }, + triggered_by: null, + triggered: [], + }, + pipelineScheduleUrl: 'foo', + pipelineKey: 'id', + viewType: 'root', + }; +}; + +export const mockPipelineNoCommit = () => { + return { + pipeline: { + id: 268, + iid: 34, + user: { + id: 1, + username: 'root', + name: 'Administrator', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'http://gdk.test:3000/root', + show_status: false, + path: '/root', + }, + active: false, + source: 'push', + created_at: '2022-01-14T17:40:27.866Z', + updated_at: '2022-01-14T18:02:35.850Z', + path: '/root/mr-widgets/-/pipelines/268', + flags: { + stuck: false, + auto_devops: false, + merge_request: false, + yaml_errors: false, + retryable: true, + cancelable: false, + failure_reason: false, + detached_merge_request_pipeline: false, + merge_request_pipeline: false, + merge_train_pipeline: false, + latest: true, + }, + details: { + status: { + icon: 'status_warning', + text: 'passed', + label: 'passed with warnings', + group: 'success-with-warnings', + tooltip: 'passed', + has_details: true, + details_path: '/root/mr-widgets/-/pipelines/268', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + stages: [ + { + name: 'validate', + title: 'validate: passed with warnings', + status: { + icon: 'status_warning', + text: 'passed', + label: 'passed with warnings', + group: 'success-with-warnings', + tooltip: 'passed', + has_details: true, + details_path: '/root/mr-widgets/-/pipelines/268#validate', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + path: '/root/mr-widgets/-/pipelines/268#validate', + dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=validate', + }, + { + name: 'test', + title: 'test: passed', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/root/mr-widgets/-/pipelines/268#test', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + path: '/root/mr-widgets/-/pipelines/268#test', + dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=test', + }, + { + name: 'build', + title: 'build: passed', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/root/mr-widgets/-/pipelines/268#build', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + path: '/root/mr-widgets/-/pipelines/268#build', + dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=build', + }, + ], + duration: 75, + finished_at: '2022-01-14T18:02:35.842Z', + name: 'Pipeline', + manual_actions: [], + scheduled_actions: [], + }, + ref: { + name: 'update-ci', + path: '/root/mr-widgets/-/commits/update-ci', + tag: false, + branch: true, + merge_request: false, + }, + retry_path: '/root/mr-widgets/-/pipelines/268/retry', + delete_path: '/root/mr-widgets/-/pipelines/268', + failed_builds: [ + { + id: 1260, + name: 'fmt', + started: '2022-01-14T17:40:36.435Z', + complete: true, + archived: false, + build_path: '/root/mr-widgets/-/jobs/1260', + retry_path: '/root/mr-widgets/-/jobs/1260/retry', + playable: false, + scheduled: false, + created_at: '2022-01-14T17:40:27.879Z', + updated_at: '2022-01-14T17:40:42.129Z', + status: { + icon: 'status_warning', + text: 'failed', + label: 'failed (allowed to fail)', + group: 'failed-with-warnings', + tooltip: 'failed - (script failure) (allowed to fail)', + has_details: true, + details_path: '/root/mr-widgets/-/jobs/1260', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/mr-widgets/-/jobs/1260/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + recoverable: false, + }, + ], + project: { + id: 23, + name: 'mr-widgets', + full_path: '/root/mr-widgets', + full_name: 'Administrator / mr-widgets', + }, + triggered_by: null, + triggered: [], + }, + pipelineScheduleUrl: 'foo', + pipelineKey: 'id', + viewType: 'root', + }; +}; diff --git a/spec/frontend/pipelines/notification/deprecated_type_keyword_notification_spec.js b/spec/frontend/pipelines/notification/deprecated_type_keyword_notification_spec.js new file mode 100644 index 00000000000..f626652a944 --- /dev/null +++ b/spec/frontend/pipelines/notification/deprecated_type_keyword_notification_spec.js @@ -0,0 +1,146 @@ +import VueApollo from 'vue-apollo'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { GlAlert, GlSprintf } from '@gitlab/ui'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import DeprecatedTypeKeywordNotification from '~/pipelines/components/notification/deprecated_type_keyword_notification.vue'; +import getPipelineWarnings from '~/pipelines/graphql/queries/get_pipeline_warnings.query.graphql'; +import { + mockWarningsWithoutDeprecation, + mockWarningsRootType, + mockWarningsType, + mockWarningsTypesAll, +} from './mock_data'; + +const defaultProvide = { + deprecatedKeywordsDocPath: '/help/ci/yaml/index.md#deprecated-keywords', + fullPath: '/namespace/my-project', + pipelineIid: 4, +}; + +let wrapper; + +const mockWarnings = jest.fn(); + +const createComponent = ({ isLoading = false, options = {} } = {}) => { + return shallowMount(DeprecatedTypeKeywordNotification, { + stubs: { + GlSprintf, + }, + provide: { + ...defaultProvide, + }, + mocks: { + $apollo: { + queries: { + warnings: { + loading: isLoading, + }, + }, + }, + }, + ...options, + }); +}; + +const createComponentWithApollo = () => { + const localVue = createLocalVue(); + localVue.use(VueApollo); + + const handlers = [[getPipelineWarnings, mockWarnings]]; + const mockApollo = createMockApollo(handlers); + + return createComponent({ + options: { + localVue, + apolloProvider: mockApollo, + mocks: {}, + }, + }); +}; + +const findAlert = () => wrapper.findComponent(GlAlert); +const findAlertItems = () => findAlert().findAll('li'); + +afterEach(() => { + wrapper.destroy(); +}); + +describe('Deprecated keyword notification', () => { + describe('while loading the pipeline warnings', () => { + beforeEach(() => { + wrapper = createComponent({ isLoading: true }); + }); + + it('does not display the notification', () => { + expect(findAlert().exists()).toBe(false); + }); + }); + + describe('if there is an error in the query', () => { + beforeEach(async () => { + mockWarnings.mockResolvedValue({ errors: ['It didnt work'] }); + wrapper = createComponentWithApollo(); + await waitForPromises(); + }); + + it('does not display the notification', () => { + expect(findAlert().exists()).toBe(false); + }); + }); + + describe('with a valid query result', () => { + describe('if there are no deprecation warnings', () => { + beforeEach(async () => { + mockWarnings.mockResolvedValue(mockWarningsWithoutDeprecation); + wrapper = createComponentWithApollo(); + await waitForPromises(); + }); + it('does not show the notification', () => { + expect(findAlert().exists()).toBe(false); + }); + }); + + describe('with a root type deprecation message', () => { + beforeEach(async () => { + mockWarnings.mockResolvedValue(mockWarningsRootType); + wrapper = createComponentWithApollo(); + await waitForPromises(); + }); + it('shows the notification with one item', () => { + expect(findAlert().exists()).toBe(true); + expect(findAlertItems()).toHaveLength(1); + expect(findAlertItems().at(0).text()).toContain('types'); + }); + }); + + describe('with a job type deprecation message', () => { + beforeEach(async () => { + mockWarnings.mockResolvedValue(mockWarningsType); + wrapper = createComponentWithApollo(); + await waitForPromises(); + }); + it('shows the notification with one item', () => { + expect(findAlert().exists()).toBe(true); + expect(findAlertItems()).toHaveLength(1); + expect(findAlertItems().at(0).text()).toContain('type'); + expect(findAlertItems().at(0).text()).not.toContain('types'); + }); + }); + + describe('with both the root types and job type deprecation message', () => { + beforeEach(async () => { + mockWarnings.mockResolvedValue(mockWarningsTypesAll); + wrapper = createComponentWithApollo(); + await waitForPromises(); + }); + it('shows the notification with two items', () => { + expect(findAlert().exists()).toBe(true); + expect(findAlertItems()).toHaveLength(2); + expect(findAlertItems().at(0).text()).toContain('types'); + expect(findAlertItems().at(1).text()).toContain('type'); + expect(findAlertItems().at(1).text()).not.toContain('types'); + }); + }); + }); +}); diff --git a/spec/frontend/pipelines/notification/mock_data.js b/spec/frontend/pipelines/notification/mock_data.js new file mode 100644 index 00000000000..e36f391a854 --- /dev/null +++ b/spec/frontend/pipelines/notification/mock_data.js @@ -0,0 +1,33 @@ +const randomWarning = { + content: 'another random warning', + id: 'gid://gitlab/Ci::PipelineMessage/272', +}; + +const rootTypeWarning = { + content: 'root `types` will be removed in 15.0.', + id: 'gid://gitlab/Ci::PipelineMessage/273', +}; + +const typeWarning = { + content: '`type` will be removed in 15.0.', + id: 'gid://gitlab/Ci::PipelineMessage/274', +}; + +function createWarningMock(warnings) { + return { + data: { + project: { + id: 'gid://gitlab/Project/28"', + pipeline: { + id: 'gid://gitlab/Ci::Pipeline/183', + warningMessages: warnings, + }, + }, + }, + }; +} + +export const mockWarningsWithoutDeprecation = createWarningMock([randomWarning]); +export const mockWarningsRootType = createWarningMock([rootTypeWarning]); +export const mockWarningsType = createWarningMock([typeWarning]); +export const mockWarningsTypesAll = createWarningMock([rootTypeWarning, typeWarning]); diff --git a/spec/frontend/pipelines/pipeline_triggerer_spec.js b/spec/frontend/pipelines/pipeline_triggerer_spec.js index ffb2721f159..701b1691c7b 100644 --- a/spec/frontend/pipelines/pipeline_triggerer_spec.js +++ b/spec/frontend/pipelines/pipeline_triggerer_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import pipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; @@ -47,15 +48,14 @@ describe('Pipelines Triggerer', () => { }); }); - it('should render "API" when no triggerer is provided', () => { + it('should render "API" when no triggerer is provided', async () => { wrapper.setProps({ pipeline: { user: null, }, }); - return wrapper.vm.$nextTick(() => { - expect(wrapper.find('.js-pipeline-url-api').text()).toEqual('API'); - }); + await nextTick(); + expect(wrapper.find('.js-pipeline-url-api').text()).toEqual('API'); }); }); diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js index 912b5afe0e1..b24e2e09ea8 100644 --- a/spec/frontend/pipelines/pipeline_url_spec.js +++ b/spec/frontend/pipelines/pipeline_url_spec.js @@ -1,41 +1,48 @@ -import { shallowMount } from '@vue/test-utils'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { trimText } from 'helpers/text_helper'; import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue'; +import { + mockPipeline, + mockPipelineBranch, + mockPipelineTag, + mockPipelineNoCommit, +} from './mock_data'; const projectPath = 'test/test'; describe('Pipeline Url Component', () => { let wrapper; - const findTableCell = () => wrapper.find('[data-testid="pipeline-url-table-cell"]'); - const findPipelineUrlLink = () => wrapper.find('[data-testid="pipeline-url-link"]'); - const findScheduledTag = () => wrapper.find('[data-testid="pipeline-url-scheduled"]'); - const findLatestTag = () => wrapper.find('[data-testid="pipeline-url-latest"]'); - const findYamlTag = () => wrapper.find('[data-testid="pipeline-url-yaml"]'); - const findFailureTag = () => wrapper.find('[data-testid="pipeline-url-failure"]'); - const findAutoDevopsTag = () => wrapper.find('[data-testid="pipeline-url-autodevops"]'); - const findAutoDevopsTagLink = () => wrapper.find('[data-testid="pipeline-url-autodevops-link"]'); - const findStuckTag = () => wrapper.find('[data-testid="pipeline-url-stuck"]'); - const findDetachedTag = () => wrapper.find('[data-testid="pipeline-url-detached"]'); - const findForkTag = () => wrapper.find('[data-testid="pipeline-url-fork"]'); - const findTrainTag = () => wrapper.find('[data-testid="pipeline-url-train"]'); - - const defaultProps = { - pipeline: { - id: 1, - path: 'foo', - project: { full_path: `/${projectPath}` }, - flags: {}, - }, - pipelineScheduleUrl: 'foo', - pipelineKey: 'id', - }; - - const createComponent = (props) => { - wrapper = shallowMount(PipelineUrlComponent, { + const findTableCell = () => wrapper.findByTestId('pipeline-url-table-cell'); + const findPipelineUrlLink = () => wrapper.findByTestId('pipeline-url-link'); + const findScheduledTag = () => wrapper.findByTestId('pipeline-url-scheduled'); + const findLatestTag = () => wrapper.findByTestId('pipeline-url-latest'); + const findYamlTag = () => wrapper.findByTestId('pipeline-url-yaml'); + const findFailureTag = () => wrapper.findByTestId('pipeline-url-failure'); + const findAutoDevopsTag = () => wrapper.findByTestId('pipeline-url-autodevops'); + const findAutoDevopsTagLink = () => wrapper.findByTestId('pipeline-url-autodevops-link'); + const findStuckTag = () => wrapper.findByTestId('pipeline-url-stuck'); + const findDetachedTag = () => wrapper.findByTestId('pipeline-url-detached'); + const findForkTag = () => wrapper.findByTestId('pipeline-url-fork'); + const findTrainTag = () => wrapper.findByTestId('pipeline-url-train'); + const findRefName = () => wrapper.findByTestId('merge-request-ref'); + const findCommitShortSha = () => wrapper.findByTestId('commit-short-sha'); + const findCommitIcon = () => wrapper.findByTestId('commit-icon'); + const findCommitIconType = () => wrapper.findByTestId('commit-icon-type'); + + const findCommitTitleContainer = () => wrapper.findByTestId('commit-title-container'); + const findCommitTitle = () => wrapper.findByTestId('commit-title'); + + const defaultProps = mockPipeline(projectPath); + + const createComponent = (props, rearrangePipelinesTable = false) => { + wrapper = shallowMountExtended(PipelineUrlComponent, { propsData: { ...defaultProps, ...props }, provide: { targetProjectFullPath: projectPath, + glFeatures: { + rearrangePipelinesTable, + }, }, }); }; @@ -45,158 +52,218 @@ describe('Pipeline Url Component', () => { wrapper = null; }); - it('should render pipeline url table cell', () => { - createComponent(); + describe('with the rearrangePipelinesTable feature flag turned off', () => { + it('should render pipeline url table cell', () => { + createComponent(); - expect(findTableCell().exists()).toBe(true); - }); + expect(findTableCell().exists()).toBe(true); + }); - it('should render a link the provided path and id', () => { - createComponent(); + it('should render a link the provided path and id', () => { + createComponent(); - expect(findPipelineUrlLink().attributes('href')).toBe('foo'); + expect(findPipelineUrlLink().attributes('href')).toBe('foo'); - expect(findPipelineUrlLink().text()).toBe('#1'); - }); + expect(findPipelineUrlLink().text()).toBe('#1'); + }); - it('should not render tags when flags are not set', () => { - createComponent(); - - expect(findStuckTag().exists()).toBe(false); - expect(findLatestTag().exists()).toBe(false); - expect(findYamlTag().exists()).toBe(false); - expect(findAutoDevopsTag().exists()).toBe(false); - expect(findFailureTag().exists()).toBe(false); - expect(findScheduledTag().exists()).toBe(false); - expect(findForkTag().exists()).toBe(false); - expect(findTrainTag().exists()).toBe(false); - }); + it('should not render tags when flags are not set', () => { + createComponent(); + + expect(findStuckTag().exists()).toBe(false); + expect(findLatestTag().exists()).toBe(false); + expect(findYamlTag().exists()).toBe(false); + expect(findAutoDevopsTag().exists()).toBe(false); + expect(findFailureTag().exists()).toBe(false); + expect(findScheduledTag().exists()).toBe(false); + expect(findForkTag().exists()).toBe(false); + expect(findTrainTag().exists()).toBe(false); + }); - it('should render the stuck tag when flag is provided', () => { - createComponent({ - pipeline: { - flags: { - stuck: true, - }, - }, + it('should render the stuck tag when flag is provided', () => { + const stuckPipeline = defaultProps.pipeline; + stuckPipeline.flags.stuck = true; + + createComponent({ + ...stuckPipeline.pipeline, + }); + + expect(findStuckTag().text()).toContain('stuck'); }); - expect(findStuckTag().text()).toContain('stuck'); - }); + it('should render latest tag when flag is provided', () => { + const latestPipeline = defaultProps.pipeline; + latestPipeline.flags.latest = true; - it('should render latest tag when flag is provided', () => { - createComponent({ - pipeline: { - flags: { - latest: true, - }, - }, + createComponent({ + ...latestPipeline, + }); + + expect(findLatestTag().text()).toContain('latest'); }); - expect(findLatestTag().text()).toContain('latest'); - }); + it('should render a yaml badge when it is invalid', () => { + const yamlPipeline = defaultProps.pipeline; + yamlPipeline.flags.yaml_errors = true; - it('should render a yaml badge when it is invalid', () => { - createComponent({ - pipeline: { - flags: { - yaml_errors: true, - }, - }, + createComponent({ + ...yamlPipeline, + }); + + expect(findYamlTag().text()).toContain('yaml invalid'); }); - expect(findYamlTag().text()).toContain('yaml invalid'); - }); + it('should render an autodevops badge when flag is provided', () => { + const autoDevopsPipeline = defaultProps.pipeline; + autoDevopsPipeline.flags.auto_devops = true; - it('should render an autodevops badge when flag is provided', () => { - createComponent({ - pipeline: { - ...defaultProps.pipeline, - flags: { - auto_devops: true, - }, - }, + createComponent({ + ...autoDevopsPipeline, + }); + + expect(trimText(findAutoDevopsTag().text())).toBe('Auto DevOps'); + + expect(findAutoDevopsTagLink().attributes()).toMatchObject({ + href: '/help/topics/autodevops/index.md', + target: '_blank', + }); }); - expect(trimText(findAutoDevopsTag().text())).toBe('Auto DevOps'); + it('should render a detached badge when flag is provided', () => { + const detachedMRPipeline = defaultProps.pipeline; + detachedMRPipeline.flags.detached_merge_request_pipeline = true; - expect(findAutoDevopsTagLink().attributes()).toMatchObject({ - href: '/help/topics/autodevops/index.md', - target: '_blank', + createComponent({ + ...detachedMRPipeline, + }); + + expect(findDetachedTag().text()).toContain('detached'); }); - }); - it('should render a detached badge when flag is provided', () => { - createComponent({ - pipeline: { - flags: { - detached_merge_request_pipeline: true, - }, - }, + it('should render error badge when pipeline has a failure reason set', () => { + const failedPipeline = defaultProps.pipeline; + failedPipeline.flags.failure_reason = true; + failedPipeline.failure_reason = 'some reason'; + + createComponent({ + ...failedPipeline, + }); + + expect(findFailureTag().text()).toContain('error'); + expect(findFailureTag().attributes('title')).toContain('some reason'); }); - expect(findDetachedTag().text()).toContain('detached'); - }); + it('should render scheduled badge when pipeline was triggered by a schedule', () => { + const scheduledPipeline = defaultProps.pipeline; + scheduledPipeline.source = 'schedule'; - it('should render error badge when pipeline has a failure reason set', () => { - createComponent({ - pipeline: { - flags: { - failure_reason: true, - }, - failure_reason: 'some reason', - }, + createComponent({ + ...scheduledPipeline, + }); + + expect(findScheduledTag().exists()).toBe(true); + expect(findScheduledTag().text()).toContain('Scheduled'); }); - expect(findFailureTag().text()).toContain('error'); - expect(findFailureTag().attributes('title')).toContain('some reason'); - }); + it('should render the fork badge when the pipeline was run in a fork', () => { + const forkedPipeline = defaultProps.pipeline; + forkedPipeline.project.full_path = '/test/forked'; - it('should render scheduled badge when pipeline was triggered by a schedule', () => { - createComponent({ - pipeline: { - flags: {}, - source: 'schedule', - }, + createComponent({ + ...forkedPipeline, + }); + + expect(findForkTag().exists()).toBe(true); + expect(findForkTag().text()).toBe('fork'); }); - expect(findScheduledTag().exists()).toBe(true); - expect(findScheduledTag().text()).toContain('Scheduled'); - }); + it('should render the train badge when the pipeline is a merge train pipeline', () => { + const mergeTrainPipeline = defaultProps.pipeline; + mergeTrainPipeline.flags.merge_train_pipeline = true; - it('should render the fork badge when the pipeline was run in a fork', () => { - createComponent({ - pipeline: { - flags: {}, - project: { fullPath: '/test/forked' }, - }, + createComponent({ + ...mergeTrainPipeline, + }); + + expect(findTrainTag().text()).toContain('train'); }); - expect(findForkTag().exists()).toBe(true); - expect(findForkTag().text()).toBe('fork'); - }); + it('should not render the train badge when the pipeline is not a merge train pipeline', () => { + const mergeTrainPipeline = defaultProps.pipeline; + mergeTrainPipeline.flags.merge_train_pipeline = false; - it('should render the train badge when the pipeline is a merge train pipeline', () => { - createComponent({ - pipeline: { - flags: { - merge_train_pipeline: true, - }, - }, + createComponent({ + ...mergeTrainPipeline, + }); + + expect(findTrainTag().exists()).toBe(false); }); - expect(findTrainTag().text()).toContain('train'); + it('should not render the commit wrapper and commit-short-sha', () => { + createComponent(); + + expect(findCommitTitleContainer().exists()).toBe(false); + expect(findCommitShortSha().exists()).toBe(false); + }); }); - it('should not render the train badge when the pipeline is not a merge train pipeline', () => { - createComponent({ - pipeline: { - flags: { - merge_train_pipeline: false, - }, + describe('with the rearrangePipelinesTable feature flag turned on', () => { + it('should render the commit title, commit reference and commit-short-sha', () => { + createComponent({}, true); + + const commitWrapper = findCommitTitleContainer(); + + expect(findCommitTitle(commitWrapper).exists()).toBe(true); + expect(findRefName().exists()).toBe(true); + expect(findCommitShortSha().exists()).toBe(true); + }); + + it('should render commit icon tooltip', () => { + createComponent({}, true); + + expect(findCommitIcon().attributes('title')).toBe('Commit'); + }); + + it.each` + pipeline | expectedTitle + ${mockPipelineTag()} | ${'Tag'} + ${mockPipelineBranch()} | ${'Branch'} + ${mockPipeline()} | ${'Merge Request'} + `( + 'should render tooltip $expectedTitle for commit icon type', + ({ pipeline, expectedTitle }) => { + createComponent(pipeline, true); + + expect(findCommitIconType().attributes('title')).toBe(expectedTitle); }, + ); + + describe('with commit', () => { + beforeEach(() => { + createComponent({}, true); + }); + + it('displays commit title with link to pipeline', () => { + expect(findCommitTitle().attributes('href')).toBe(defaultProps.pipeline.path); + }); + + it('displays commit title text', () => { + expect(findCommitTitle().text()).toBe(defaultProps.pipeline.commit.title); + }); }); - expect(findTrainTag().exists()).toBe(false); + describe('without commit', () => { + beforeEach(() => { + createComponent(mockPipelineNoCommit(), true); + }); + + it('displays cant find head commit text', () => { + expect(findCommitTitle().text()).toBe("Can't find HEAD commit for this branch"); + }); + + it('displays link to pipeline', () => { + expect(findCommitTitle().attributes('href')).toBe(mockPipelineNoCommit().pipeline.path); + }); + }); }); }); diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js index c4bfec8ae14..9b2ee6b8278 100644 --- a/spec/frontend/pipelines/pipelines_actions_spec.js +++ b/spec/frontend/pipelines/pipelines_actions_spec.js @@ -1,14 +1,21 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import { TEST_HOST } from 'spec/test_constants'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; +import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipelines_manual_actions.vue'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; jest.mock('~/flash'); +jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => { + return { + confirmAction: jest.fn(), + }; +}); describe('Pipelines Actions dropdown', () => { let wrapper; @@ -35,6 +42,7 @@ describe('Pipelines Actions dropdown', () => { wrapper = null; mock.restore(); + confirmAction.mockReset(); }); describe('manual actions', () => { @@ -68,7 +76,7 @@ describe('Pipelines Actions dropdown', () => { findAllDropdownItems().at(0).vm.$emit('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findDropdown().props('loading')).toBe(true); await waitForPromises(); @@ -80,7 +88,7 @@ describe('Pipelines Actions dropdown', () => { findAllDropdownItems().at(0).vm.$emit('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findDropdown().props('loading')).toBe(true); await waitForPromises(); @@ -111,11 +119,11 @@ describe('Pipelines Actions dropdown', () => { it('makes post request after confirming', async () => { mock.onPost(scheduledJobAction.path).reply(200); - jest.spyOn(window, 'confirm').mockReturnValue(true); + confirmAction.mockResolvedValueOnce(true); findAllDropdownItems().at(0).vm.$emit('click'); - expect(window.confirm).toHaveBeenCalled(); + expect(confirmAction).toHaveBeenCalled(); await waitForPromises(); @@ -124,11 +132,11 @@ describe('Pipelines Actions dropdown', () => { it('does not make post request if confirmation is cancelled', async () => { mock.onPost(scheduledJobAction.path).reply(200); - jest.spyOn(window, 'confirm').mockReturnValue(false); + confirmAction.mockResolvedValueOnce(false); findAllDropdownItems().at(0).vm.$emit('click'); - expect(window.confirm).toHaveBeenCalled(); + expect(confirmAction).toHaveBeenCalled(); await waitForPromises(); diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js index 6fdbe907aed..f200d683a7a 100644 --- a/spec/frontend/pipelines/pipelines_table_spec.js +++ b/spec/frontend/pipelines/pipelines_table_spec.js @@ -9,7 +9,11 @@ import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_tr import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue'; import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue'; import PipelinesTimeago from '~/pipelines/components/pipelines_list/time_ago.vue'; -import { PipelineKeyOptions } from '~/pipelines/constants'; +import { + PipelineKeyOptions, + BUTTON_TOOLTIP_RETRY, + BUTTON_TOOLTIP_CANCEL, +} from '~/pipelines/constants'; import eventHub from '~/pipelines/event_hub'; import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; @@ -33,13 +37,18 @@ describe('Pipelines Table', () => { return pipelines.find((p) => p.user !== null && p.commit !== null); }; - const createComponent = (props = {}) => { + const createComponent = (props = {}, rearrangePipelinesTable = false) => { wrapper = extendedWrapper( mount(PipelinesTable, { propsData: { ...defaultProps, ...props, }, + provide: { + glFeatures: { + rearrangePipelinesTable, + }, + }, }), ); }; @@ -61,6 +70,8 @@ describe('Pipelines Table', () => { const findStagesTh = () => wrapper.findByTestId('stages-th'); const findTimeAgoTh = () => wrapper.findByTestId('timeago-th'); const findActionsTh = () => wrapper.findByTestId('actions-th'); + const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button'); + const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button'); beforeEach(() => { pipeline = createMockPipeline(); @@ -71,7 +82,7 @@ describe('Pipelines Table', () => { wrapper = null; }); - describe('Pipelines Table', () => { + describe('Pipelines Table with rearrangePipelinesTable feature flag turned off', () => { beforeEach(() => { createComponent({ pipelines: [pipeline], viewType: 'root' }); }); @@ -187,6 +198,39 @@ describe('Pipelines Table', () => { it('should render pipeline operations', () => { expect(findActions().exists()).toBe(true); }); + + it('should render retry action tooltip', () => { + expect(findRetryBtn().attributes('title')).toBe(BUTTON_TOOLTIP_RETRY); + }); + + it('should render cancel action tooltip', () => { + expect(findCancelBtn().attributes('title')).toBe(BUTTON_TOOLTIP_CANCEL); + }); + }); + }); + + describe('Pipelines Table with rearrangePipelinesTable feature flag turned on', () => { + beforeEach(() => { + createComponent({ pipelines: [pipeline], viewType: 'root' }, true); + }); + + it('should render table head with correct columns', () => { + expect(findStatusTh().text()).toBe('Status'); + expect(findPipelineTh().text()).toBe('Pipeline'); + expect(findStagesTh().text()).toBe('Stages'); + expect(findActionsTh().text()).toBe('Actions'); + }); + + describe('triggerer cell', () => { + it('should render the pipeline triggerer', () => { + expect(findTriggerer().exists()).toBe(true); + }); + }); + + describe('commit cell', () => { + it('should not render commit information', () => { + expect(findCommit().exists()).toBe(false); + }); }); }); }); 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 c995eb864d1..4b33c1522a5 100644 --- a/spec/frontend/pipelines/test_reports/test_case_details_spec.js +++ b/spec/frontend/pipelines/test_reports/test_case_details_spec.js @@ -1,11 +1,9 @@ import { GlModal } from '@gitlab/ui'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount } 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'; -const localVue = createLocalVue(); - describe('Test case details', () => { let wrapper; const defaultTestCase = { @@ -29,7 +27,6 @@ describe('Test case details', () => { const createComponent = (testCase = {}) => { wrapper = extendedWrapper( shallowMount(TestCaseDetails, { - localVue, propsData: { modalId: 'my-modal', testCase: { diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js index 384b7cf6930..e0daf8cb4b5 100644 --- a/spec/frontend/pipelines/test_reports/test_reports_spec.js +++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js @@ -1,5 +1,6 @@ import { GlLoadingIcon } from '@gitlab/ui'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import Vuex from 'vuex'; import testReports from 'test_fixtures/pipelines/test_report.json'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; @@ -9,8 +10,7 @@ import TestSummary from '~/pipelines/components/test_reports/test_summary.vue'; import TestSummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue'; import * as getters from '~/pipelines/stores/test_reports/getters'; -const localVue = createLocalVue(); -localVue.use(Vuex); +Vue.use(Vuex); describe('Test reports app', () => { let wrapper; @@ -44,7 +44,6 @@ describe('Test reports app', () => { wrapper = extendedWrapper( shallowMount(TestReports, { store, - localVue, }), ); }; diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js index 793bad6b82a..97241e14129 100644 --- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js +++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js @@ -1,5 +1,6 @@ import { GlButton, GlFriendlyWrap, GlLink, GlPagination } from '@gitlab/ui'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import Vuex from 'vuex'; import testReports from 'test_fixtures/pipelines/test_report.json'; import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue'; @@ -8,8 +9,7 @@ import * as getters from '~/pipelines/stores/test_reports/getters'; import { formatFilePath } from '~/pipelines/stores/test_reports/utils'; import skippedTestCases from './mock_data'; -const localVue = createLocalVue(); -localVue.use(Vuex); +Vue.use(Vuex); describe('Test reports suite table', () => { let wrapper; @@ -47,7 +47,6 @@ describe('Test reports suite table', () => { wrapper = shallowMount(SuiteTable, { store, - localVue, stubs: { GlFriendlyWrap }, }); }; diff --git a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js index 0813739d72f..1598d5c337f 100644 --- a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js +++ b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js @@ -1,11 +1,11 @@ -import { mount, createLocalVue } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; +import Vue from 'vue'; import Vuex from 'vuex'; import testReports from 'test_fixtures/pipelines/test_report.json'; import SummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue'; import * as getters from '~/pipelines/stores/test_reports/getters'; -const localVue = createLocalVue(); -localVue.use(Vuex); +Vue.use(Vuex); describe('Test reports summary table', () => { let wrapper; @@ -29,7 +29,6 @@ describe('Test reports summary table', () => { wrapper = mount(SummaryTable, { propsData: defaultProps, store, - localVue, }); }; |