diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-21 07:08:36 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-21 07:08:36 +0000 |
commit | 48aff82709769b098321c738f3444b9bdaa694c6 (patch) | |
tree | e00c7c43e2d9b603a5a6af576b1685e400410dee /spec/frontend/pipelines | |
parent | 879f5329ee916a948223f8f43d77fba4da6cd028 (diff) | |
download | gitlab-ce-48aff82709769b098321c738f3444b9bdaa694c6.tar.gz |
Add latest changes from gitlab-org/gitlab@13-5-stable-eev13.5.0-rc42
Diffstat (limited to 'spec/frontend/pipelines')
16 files changed, 804 insertions, 458 deletions
diff --git a/spec/frontend/pipelines/components/dag/dag_graph_spec.js b/spec/frontend/pipelines/components/dag/dag_graph_spec.js index e312791b01f..7786212cb69 100644 --- a/spec/frontend/pipelines/components/dag/dag_graph_spec.js +++ b/spec/frontend/pipelines/components/dag/dag_graph_spec.js @@ -3,7 +3,7 @@ import DagGraph from '~/pipelines/components/dag/dag_graph.vue'; import { IS_HIGHLIGHTED, LINK_SELECTOR, NODE_SELECTOR } from '~/pipelines/components/dag/constants'; import { highlightIn, highlightOut } from '~/pipelines/components/dag/interactions'; import { createSankey } from '~/pipelines/components/dag/drawing_utils'; -import { removeOrphanNodes } from '~/pipelines/components/dag/parsing_utils'; +import { removeOrphanNodes } from '~/pipelines/components/parsing_utils'; import { parsedData } from './mock_data'; describe('The DAG graph', () => { diff --git a/spec/frontend/pipelines/components/dag/dag_spec.js b/spec/frontend/pipelines/components/dag/dag_spec.js index 989f6c17197..08a43199594 100644 --- a/spec/frontend/pipelines/components/dag/dag_spec.js +++ b/spec/frontend/pipelines/components/dag/dag_spec.js @@ -4,13 +4,8 @@ import Dag from '~/pipelines/components/dag/dag.vue'; import DagGraph from '~/pipelines/components/dag/dag_graph.vue'; import DagAnnotations from '~/pipelines/components/dag/dag_annotations.vue'; -import { - ADD_NOTE, - REMOVE_NOTE, - REPLACE_NOTES, - PARSE_FAILURE, - UNSUPPORTED_DATA, -} from '~/pipelines/components/dag//constants'; +import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from '~/pipelines/components/dag/constants'; +import { PARSE_FAILURE, UNSUPPORTED_DATA } from '~/pipelines/constants'; import { mockParsedGraphQLNodes, tooSmallGraph, diff --git a/spec/frontend/pipelines/components/dag/drawing_utils_spec.js b/spec/frontend/pipelines/components/dag/drawing_utils_spec.js index 37a7d07485b..095ded01298 100644 --- a/spec/frontend/pipelines/components/dag/drawing_utils_spec.js +++ b/spec/frontend/pipelines/components/dag/drawing_utils_spec.js @@ -1,5 +1,5 @@ import { createSankey } from '~/pipelines/components/dag/drawing_utils'; -import { parseData } from '~/pipelines/components/dag/parsing_utils'; +import { parseData } from '~/pipelines/components/parsing_utils'; import { mockParsedGraphQLNodes } from './mock_data'; describe('DAG visualization drawing utilities', () => { diff --git a/spec/frontend/pipelines/components/dag/parsing_utils_spec.js b/spec/frontend/pipelines/components/dag/parsing_utils_spec.js index e93fa8e6760..ceb6b64d4ad 100644 --- a/spec/frontend/pipelines/components/dag/parsing_utils_spec.js +++ b/spec/frontend/pipelines/components/dag/parsing_utils_spec.js @@ -5,7 +5,7 @@ import { parseData, removeOrphanNodes, getMaxNodes, -} from '~/pipelines/components/dag/parsing_utils'; +} from '~/pipelines/components/parsing_utils'; import { createSankey } from '~/pipelines/components/dag/drawing_utils'; import { mockParsedGraphQLNodes } from './mock_data'; diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js index d977db58a0e..062c9759a65 100644 --- a/spec/frontend/pipelines/graph/graph_component_spec.js +++ b/spec/frontend/pipelines/graph/graph_component_spec.js @@ -3,23 +3,27 @@ import { mount } from '@vue/test-utils'; import { setHTMLFixture } from 'helpers/fixtures'; import PipelineStore from '~/pipelines/stores/pipeline_store'; import graphComponent from '~/pipelines/components/graph/graph_component.vue'; -import stageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue'; +import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue'; import linkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue'; import graphJSON from './mock_data'; import linkedPipelineJSON from './linked_pipelines_mock_data'; import PipelinesMediator from '~/pipelines/pipeline_details_mediator'; describe('graph component', () => { - const store = new PipelineStore(); - store.storePipeline(linkedPipelineJSON); - const mediator = new PipelinesMediator({ endpoint: '' }); - + let store; + let mediator; let wrapper; const findExpandPipelineBtn = () => wrapper.find('[data-testid="expandPipelineButton"]'); const findAllExpandPipelineBtns = () => wrapper.findAll('[data-testid="expandPipelineButton"]'); + const findStageColumns = () => wrapper.findAll(StageColumnComponent); + const findStageColumnAt = i => findStageColumns().at(i); beforeEach(() => { + mediator = new PipelinesMediator({ endpoint: '' }); + store = new PipelineStore(); + store.storePipeline(linkedPipelineJSON); + setHTMLFixture('<div class="layout-page"></div>'); }); @@ -43,7 +47,7 @@ describe('graph component', () => { }); describe('with data', () => { - it('should render the graph', () => { + beforeEach(() => { wrapper = mount(graphComponent, { propsData: { isLoading: false, @@ -51,26 +55,17 @@ describe('graph component', () => { mediator, }, }); + }); + it('renders the graph', () => { expect(wrapper.find('.js-pipeline-graph').exists()).toBe(true); - - expect(wrapper.find(stageColumnComponent).classes()).toContain('no-margin'); - - expect( - wrapper - .findAll(stageColumnComponent) - .at(1) - .classes(), - ).toContain('left-margin'); - - expect(wrapper.find('.stage-column:nth-child(2) .build:nth-child(1)').classes()).toContain( - 'left-connector', - ); - expect(wrapper.find('.loading-icon').exists()).toBe(false); - expect(wrapper.find('.stage-column-list').exists()).toBe(true); }); + + it('renders columns in the graph', () => { + expect(findStageColumns()).toHaveLength(graphJSON.details.stages.length); + }); }); describe('when linked pipelines are present', () => { @@ -93,26 +88,26 @@ describe('graph component', () => { expect(wrapper.find('.fa-spinner').exists()).toBe(false); }); - it('should include the stage column list', () => { - expect(wrapper.find(stageColumnComponent).exists()).toBe(true); - }); - - it('should include the no-margin class on the first child if there is only one job', () => { - const firstStageColumnElement = wrapper.find(stageColumnComponent); - - expect(firstStageColumnElement.classes()).toContain('no-margin'); + it('should include the stage column', () => { + expect(findStageColumnAt(0).exists()).toBe(true); }); - it('should include the has-only-one-job class on the first child', () => { - const firstStageColumnElement = wrapper.find('.stage-column-list .stage-column'); - - expect(firstStageColumnElement.classes()).toContain('has-only-one-job'); + it('stage column should have no-margin, gl-mr-26, has-only-one-job classes if there is only one job', () => { + expect(findStageColumnAt(0).classes()).toEqual( + expect.arrayContaining(['no-margin', 'gl-mr-26', 'has-only-one-job']), + ); }); it('should include the left-margin class on the second child', () => { - const firstStageColumnElement = wrapper.find('.stage-column-list .stage-column:last-child'); + expect(findStageColumnAt(1).classes('left-margin')).toBe(true); + }); - expect(firstStageColumnElement.classes()).toContain('left-margin'); + it('should include the left-connector class in the build of the second child', () => { + expect( + findStageColumnAt(1) + .find('.build:nth-child(1)') + .classes('left-connector'), + ).toBe(true); }); it('should include the js-has-linked-pipelines flag', () => { @@ -134,12 +129,7 @@ describe('graph component', () => { describe('stageConnectorClass', () => { it('it returns left-margin when there is a triggerer', () => { - expect( - wrapper - .findAll(stageColumnComponent) - .at(1) - .classes(), - ).toContain('left-margin'); + expect(findStageColumnAt(1).classes('left-margin')).toBe(true); }); }); }); @@ -248,6 +238,16 @@ describe('graph component', () => { .catch(done.fail); }); }); + + describe('when column requests a refresh', () => { + beforeEach(() => { + findStageColumnAt(0).vm.$emit('refreshPipelineGraph'); + }); + + it('refreshPipelineGraph is emitted', () => { + expect(wrapper.emitted().refreshPipelineGraph).toHaveLength(1); + }); + }); }); }); }); @@ -268,7 +268,7 @@ describe('graph component', () => { it('should include the first column with a no margin', () => { const firstColumn = wrapper.find('.stage-column'); - expect(firstColumn.classes()).toContain('no-margin'); + expect(firstColumn.classes('no-margin')).toBe(true); }); it('should not render a linked pipelines column', () => { @@ -278,16 +278,11 @@ describe('graph component', () => { describe('stageConnectorClass', () => { it('it returns no-margin when no triggerer and there is one job', () => { - expect(wrapper.find(stageColumnComponent).classes()).toContain('no-margin'); + expect(findStageColumnAt(0).classes('no-margin')).toBe(true); }); it('it returns left-margin when no triggerer and not the first stage', () => { - expect( - wrapper - .findAll(stageColumnComponent) - .at(1) - .classes(), - ).toContain('left-margin'); + expect(findStageColumnAt(1).classes('left-margin')).toBe(true); }); }); }); @@ -302,12 +297,9 @@ describe('graph component', () => { }, }); - expect( - wrapper - .find('.stage-column:nth-child(2) .stage-name') - .text() - .trim(), - ).toEqual('Deploy <img src=x onerror=alert(document.domain)>'); + expect(findStageColumnAt(1).props('title')).toEqual( + 'Deploy <img src=x onerror=alert(document.domain)>', + ); }); }); }); diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js index e844cbc5bf8..8aabb2f9cdd 100644 --- a/spec/frontend/pipelines/graph/job_item_spec.js +++ b/spec/frontend/pipelines/graph/job_item_spec.js @@ -1,5 +1,4 @@ import { mount } from '@vue/test-utils'; -import { trimText } from 'helpers/text_helper'; import JobItem from '~/pipelines/components/graph/job_item.vue'; describe('pipeline graph job item', () => { @@ -65,7 +64,7 @@ describe('pipeline graph job item', () => { expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); - expect(trimText(wrapper.find('.ci-status-text').text())).toBe(mockJob.name); + expect(wrapper.text()).toBe(mockJob.name); done(); }); @@ -85,7 +84,7 @@ describe('pipeline graph job item', () => { expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); expect(wrapper.find('a').exists()).toBe(false); - expect(trimText(wrapper.find('.ci-status-text').text())).toBe(mockJobWithoutDetails.name); + expect(wrapper.text()).toBe(mockJobWithoutDetails.name); }); it('should apply hover class and provided class name', () => { diff --git a/spec/frontend/pipelines/graph/job_name_component_spec.js b/spec/frontend/pipelines/graph/job_name_component_spec.js index 3574b66403e..f0aa646b8d7 100644 --- a/spec/frontend/pipelines/graph/job_name_component_spec.js +++ b/spec/frontend/pipelines/graph/job_name_component_spec.js @@ -21,12 +21,7 @@ describe('job name component', () => { }); it('should render the provided name', () => { - expect( - wrapper - .find('.ci-status-text') - .text() - .trim(), - ).toBe(propsData.name); + expect(wrapper.text()).toBe(propsData.name); }); it('should render an icon with the provided status', () => { diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js index 5388d624d3c..2e10b0f068c 100644 --- a/spec/frontend/pipelines/header_component_spec.js +++ b/spec/frontend/pipelines/header_component_spec.js @@ -1,115 +1,164 @@ import { shallowMount } from '@vue/test-utils'; -import { GlModal } from '@gitlab/ui'; +import { GlModal, GlLoadingIcon } from '@gitlab/ui'; +import MockAdapter from 'axios-mock-adapter'; +import { + mockCancelledPipelineHeader, + mockFailedPipelineHeader, + mockRunningPipelineHeader, + mockSuccessfulPipelineHeader, +} from './mock_data'; +import axios from '~/lib/utils/axios_utils'; import HeaderComponent from '~/pipelines/components/header_component.vue'; -import CiHeader from '~/vue_shared/components/header_ci_component.vue'; -import eventHub from '~/pipelines/event_hub'; describe('Pipeline details header', () => { let wrapper; let glModalDirective; - - const threeWeeksAgo = new Date(); - threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21); + let mockAxios; const findDeleteModal = () => wrapper.find(GlModal); - - const defaultProps = { - pipeline: { - details: { - status: { - group: 'failed', - icon: 'status_failed', - label: 'failed', - text: 'failed', - details_path: 'path', - }, - }, - id: 123, - created_at: threeWeeksAgo.toISOString(), - user: { - web_url: 'path', - name: 'Foo', - username: 'foobar', - email: 'foo@bar.com', - avatar_url: 'link', - }, - retry_path: 'retry', - cancel_path: 'cancel', - delete_path: 'delete', + const findRetryButton = () => wrapper.find('[data-testid="retryPipeline"]'); + const findCancelButton = () => wrapper.find('[data-testid="cancelPipeline"]'); + const findDeleteButton = () => wrapper.find('[data-testid="deletePipeline"]'); + const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + + const defaultProvideOptions = { + pipelineId: 14, + pipelineIid: 1, + paths: { + retry: '/retry', + cancel: '/cancel', + delete: '/delete', + fullProject: '/namespace/my-project', }, - isLoading: false, }; - const createComponent = (props = {}) => { + const createComponent = (pipelineMock = mockRunningPipelineHeader, { isLoading } = false) => { glModalDirective = jest.fn(); - wrapper = shallowMount(HeaderComponent, { - propsData: { - ...props, + const $apollo = { + queries: { + pipeline: { + loading: isLoading, + stopPolling: jest.fn(), + startPolling: jest.fn(), + }, + }, + }; + + return shallowMount(HeaderComponent, { + data() { + return { + pipeline: pipelineMock, + }; + }, + provide: { + ...defaultProvideOptions, }, directives: { glModal: { - bind(el, { value }) { + bind(_, { value }) { glModalDirective(value); }, }, }, + mocks: { $apollo }, }); }; beforeEach(() => { - jest.spyOn(eventHub, '$emit'); - - createComponent(defaultProps); + mockAxios = new MockAdapter(axios); + mockAxios.onGet('*').replyOnce(200); }); afterEach(() => { - eventHub.$off(); - wrapper.destroy(); wrapper = null; + + mockAxios.restore(); }); - it('should render provided pipeline info', () => { - expect(wrapper.find(CiHeader).props()).toMatchObject({ - status: defaultProps.pipeline.details.status, - itemId: defaultProps.pipeline.id, - time: defaultProps.pipeline.created_at, - user: defaultProps.pipeline.user, + describe('initial loading', () => { + beforeEach(() => { + wrapper = createComponent(null, { isLoading: true }); }); - }); - describe('action buttons', () => { - it('should not trigger eventHub when nothing happens', () => { - expect(eventHub.$emit).not.toHaveBeenCalled(); + it('shows a loading state while graphQL is fetching initial data', () => { + expect(findLoadingIcon().exists()).toBe(true); }); + }); + + describe('visible state', () => { + it.each` + state | pipelineData | retryValue | cancelValue + ${'cancelled'} | ${mockCancelledPipelineHeader} | ${true} | ${false} + ${'failed'} | ${mockFailedPipelineHeader} | ${true} | ${false} + ${'running'} | ${mockRunningPipelineHeader} | ${false} | ${true} + ${'successful'} | ${mockSuccessfulPipelineHeader} | ${false} | ${false} + `( + 'with a $state pipeline, it will show actions: retry $retryValue and cancel $cancelValue', + ({ pipelineData, retryValue, cancelValue }) => { + wrapper = createComponent(pipelineData); + + expect(findRetryButton().exists()).toBe(retryValue); + expect(findCancelButton().exists()).toBe(cancelValue); + }, + ); + }); - it('should call postAction when retry button action is clicked', () => { - wrapper.find('[data-testid="retryButton"]').vm.$emit('click'); + describe('actions', () => { + describe('Retry action', () => { + beforeEach(() => { + wrapper = createComponent(mockCancelledPipelineHeader); + }); - expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'retry'); - }); + it('should call axios with the right path when retry button is clicked', async () => { + jest.spyOn(axios, 'post'); + findRetryButton().vm.$emit('click'); - it('should call postAction when cancel button action is clicked', () => { - wrapper.find('[data-testid="cancelPipeline"]').vm.$emit('click'); + await wrapper.vm.$nextTick(); - expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'cancel'); + expect(axios.post).toHaveBeenCalledWith(defaultProvideOptions.paths.retry); + }); }); - it('does not show delete modal', () => { - expect(findDeleteModal()).not.toBeVisible(); + describe('Cancel action', () => { + beforeEach(() => { + wrapper = createComponent(mockRunningPipelineHeader); + }); + + it('should call axios with the right path when cancel button is clicked', async () => { + jest.spyOn(axios, 'post'); + findCancelButton().vm.$emit('click'); + + await wrapper.vm.$nextTick(); + + expect(axios.post).toHaveBeenCalledWith(defaultProvideOptions.paths.cancel); + }); }); - describe('when delete button action is clicked', () => { - it('displays delete modal', () => { + describe('Delete action', () => { + beforeEach(() => { + wrapper = createComponent(mockFailedPipelineHeader); + }); + + it('displays delete modal when clicking on delete and does not call the delete action', async () => { + jest.spyOn(axios, 'delete'); + findDeleteButton().vm.$emit('click'); + + await wrapper.vm.$nextTick(); + expect(findDeleteModal().props('modalId')).toBe(wrapper.vm.$options.DELETE_MODAL_ID); expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.$options.DELETE_MODAL_ID); + expect(axios.delete).not.toHaveBeenCalled(); }); - it('should call delete when modal is submitted', () => { + it('should call delete path when modal is submitted', async () => { + jest.spyOn(axios, 'delete'); findDeleteModal().vm.$emit('ok'); - expect(eventHub.$emit).toHaveBeenCalledWith('headerDeleteAction', 'delete'); + await wrapper.vm.$nextTick(); + + expect(axios.delete).toHaveBeenCalledWith(defaultProvideOptions.paths.delete); }); }); }); diff --git a/spec/frontend/pipelines/legacy_header_component_spec.js b/spec/frontend/pipelines/legacy_header_component_spec.js new file mode 100644 index 00000000000..fb7feb8898a --- /dev/null +++ b/spec/frontend/pipelines/legacy_header_component_spec.js @@ -0,0 +1,116 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlModal } from '@gitlab/ui'; +import LegacyHeaderComponent from '~/pipelines/components/legacy_header_component.vue'; +import CiHeader from '~/vue_shared/components/header_ci_component.vue'; +import eventHub from '~/pipelines/event_hub'; + +describe('Pipeline details header', () => { + let wrapper; + let glModalDirective; + + const threeWeeksAgo = new Date(); + threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21); + + const findDeleteModal = () => wrapper.find(GlModal); + + const defaultProps = { + pipeline: { + details: { + status: { + group: 'failed', + icon: 'status_failed', + label: 'failed', + text: 'failed', + details_path: 'path', + }, + }, + id: 123, + created_at: threeWeeksAgo.toISOString(), + user: { + web_url: 'path', + name: 'Foo', + username: 'foobar', + email: 'foo@bar.com', + avatar_url: 'link', + }, + retry_path: 'retry', + cancel_path: 'cancel', + delete_path: 'delete', + }, + isLoading: false, + }; + + const createComponent = (props = {}) => { + glModalDirective = jest.fn(); + + wrapper = shallowMount(LegacyHeaderComponent, { + propsData: { + ...props, + }, + directives: { + glModal: { + bind(el, { value }) { + glModalDirective(value); + }, + }, + }, + }); + }; + + beforeEach(() => { + jest.spyOn(eventHub, '$emit'); + + createComponent(defaultProps); + }); + + afterEach(() => { + eventHub.$off(); + + wrapper.destroy(); + wrapper = null; + }); + + it('should render provided pipeline info', () => { + expect(wrapper.find(CiHeader).props()).toMatchObject({ + status: defaultProps.pipeline.details.status, + itemId: defaultProps.pipeline.id, + time: defaultProps.pipeline.created_at, + user: defaultProps.pipeline.user, + }); + }); + + describe('action buttons', () => { + it('should not trigger eventHub when nothing happens', () => { + expect(eventHub.$emit).not.toHaveBeenCalled(); + }); + + it('should call postAction when retry button action is clicked', () => { + wrapper.find('[data-testid="retryButton"]').vm.$emit('click'); + + expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'retry'); + }); + + it('should call postAction when cancel button action is clicked', () => { + wrapper.find('[data-testid="cancelPipeline"]').vm.$emit('click'); + + expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'cancel'); + }); + + it('does not show delete modal', () => { + expect(findDeleteModal()).not.toBeVisible(); + }); + + describe('when delete button action is clicked', () => { + it('displays delete modal', () => { + expect(findDeleteModal().props('modalId')).toBe(wrapper.vm.$options.DELETE_MODAL_ID); + expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.$options.DELETE_MODAL_ID); + }); + + it('should call delete when modal is submitted', () => { + findDeleteModal().vm.$emit('ok'); + + expect(eventHub.$emit).toHaveBeenCalledWith('headerDeleteAction', 'delete'); + }); + }); + }); +}); diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js index e63efc543f1..2afdbb05107 100644 --- a/spec/frontend/pipelines/mock_data.js +++ b/spec/frontend/pipelines/mock_data.js @@ -1,3 +1,7 @@ +const PIPELINE_RUNNING = 'RUNNING'; +const PIPELINE_CANCELED = 'CANCELED'; +const PIPELINE_FAILED = 'FAILED'; + export const pipelineWithStages = { id: 20333396, user: { @@ -320,6 +324,80 @@ export const pipelineWithStages = { triggered: [], }; +const threeWeeksAgo = new Date(); +threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21); + +export const mockPipelineHeader = { + detailedStatus: {}, + id: 123, + userPermissions: { + destroyPipeline: true, + }, + createdAt: threeWeeksAgo.toISOString(), + user: { + name: 'Foo', + username: 'foobar', + email: 'foo@bar.com', + avatarUrl: 'link', + }, +}; + +export const mockFailedPipelineHeader = { + ...mockPipelineHeader, + status: PIPELINE_FAILED, + retryable: true, + cancelable: false, + detailedStatus: { + group: 'failed', + icon: 'status_failed', + label: 'failed', + text: 'failed', + detailsPath: 'path', + }, +}; + +export const mockRunningPipelineHeader = { + ...mockPipelineHeader, + status: PIPELINE_RUNNING, + retryable: false, + cancelable: true, + detailedStatus: { + group: 'running', + icon: 'status_running', + label: 'running', + text: 'running', + detailsPath: 'path', + }, +}; + +export const mockCancelledPipelineHeader = { + ...mockPipelineHeader, + status: PIPELINE_CANCELED, + retryable: true, + cancelable: false, + detailedStatus: { + group: 'cancelled', + icon: 'status_cancelled', + label: 'cancelled', + text: 'cancelled', + detailsPath: 'path', + }, +}; + +export const mockSuccessfulPipelineHeader = { + ...mockPipelineHeader, + status: 'SUCCESS', + retryable: false, + cancelable: false, + detailedStatus: { + group: 'success', + icon: 'status_success', + label: 'success', + text: 'success', + detailsPath: 'path', + }, +}; + export const stageReply = { name: 'deploy', title: 'deploy: running', diff --git a/spec/frontend/pipelines/pipeline_graph/mock_data.js b/spec/frontend/pipelines/pipeline_graph/mock_data.js index 5a5d6c021a6..b50932deec6 100644 --- a/spec/frontend/pipelines/pipeline_graph/mock_data.js +++ b/spec/frontend/pipelines/pipeline_graph/mock_data.js @@ -1,3 +1,5 @@ +import { createUniqueJobId } from '~/pipelines/utils'; + export const yamlString = `stages: - empty - build @@ -39,18 +41,20 @@ deploy_a: script: echo hello `; +const jobId1 = createUniqueJobId('build', 'build_1'); +const jobId2 = createUniqueJobId('test', 'test_1'); +const jobId3 = createUniqueJobId('test', 'test_2'); +const jobId4 = createUniqueJobId('deploy', 'deploy_1'); + export const pipelineData = { stages: [ { name: 'build', - groups: [], - }, - { - name: 'build', groups: [ { name: 'build_1', jobs: [{ script: 'echo hello', stage: 'build' }], + id: jobId1, }, ], }, @@ -60,10 +64,12 @@ export const pipelineData = { { name: 'test_1', jobs: [{ script: 'yarn test', stage: 'test' }], + id: jobId2, }, { name: 'test_2', jobs: [{ script: 'yarn karma', stage: 'test' }], + id: jobId3, }, ], }, @@ -73,8 +79,15 @@ export const pipelineData = { { name: 'deploy_1', jobs: [{ script: 'yarn magick', stage: 'deploy' }], + id: jobId4, }, ], }, ], + jobs: { + [jobId1]: {}, + [jobId2]: {}, + [jobId3]: {}, + [jobId4]: {}, + }, }; diff --git a/spec/frontend/pipelines/pipeline_graph/utils_spec.js b/spec/frontend/pipelines/pipeline_graph/utils_spec.js index dd85c8c2bd0..ade026c7053 100644 --- a/spec/frontend/pipelines/pipeline_graph/utils_spec.js +++ b/spec/frontend/pipelines/pipeline_graph/utils_spec.js @@ -1,150 +1,211 @@ -import { preparePipelineGraphData } from '~/pipelines/utils'; +import { + preparePipelineGraphData, + createUniqueJobId, + generateJobNeedsDict, +} from '~/pipelines/utils'; -describe('preparePipelineGraphData', () => { - const emptyResponse = { stages: [] }; +describe('utils functions', () => { + const emptyResponse = { stages: [], jobs: {} }; const jobName1 = 'build_1'; const jobName2 = 'build_2'; const jobName3 = 'test_1'; const jobName4 = 'deploy_1'; - const job1 = { [jobName1]: { script: 'echo hello', stage: 'build' } }; - const job2 = { [jobName2]: { script: 'echo build', stage: 'build' } }; - const job3 = { [jobName3]: { script: 'echo test', stage: 'test' } }; - const job4 = { [jobName4]: { script: 'echo deploy', stage: 'deploy' } }; - - describe('returns an object with an empty array of stages if', () => { - it('no data is passed', () => { - expect(preparePipelineGraphData({})).toEqual(emptyResponse); - }); + const job1 = { script: 'echo hello', stage: 'build' }; + const job2 = { script: 'echo build', stage: 'build' }; + const job3 = { script: 'echo test', stage: 'test', needs: [jobName1, jobName2] }; + const job4 = { script: 'echo deploy', stage: 'deploy', needs: [jobName3] }; + const userDefinedStage = 'myStage'; - it('no stages are found', () => { - expect(preparePipelineGraphData({ includes: 'template/myTemplate.gitlab-ci.yml' })).toEqual( - emptyResponse, - ); - }); - }); - - describe('returns the correct array of stages', () => { - it('when multiple jobs are in the same stage', () => { - const expectedData = { - stages: [ + const pipelineGraphData = { + stages: [ + { + name: userDefinedStage, + groups: [], + }, + { + name: job4.stage, + groups: [ { - name: job1[jobName1].stage, - groups: [ - { - name: jobName1, - jobs: [{ script: job1[jobName1].script, stage: job1[jobName1].stage }], - }, - { - name: jobName2, - jobs: [{ script: job2[jobName2].script, stage: job2[jobName2].stage }], - }, - ], + name: jobName4, + jobs: [{ ...job4 }], + id: createUniqueJobId(job4.stage, jobName4), }, ], - }; - - expect(preparePipelineGraphData({ ...job1, ...job2 })).toEqual(expectedData); - }); - - it('when stages are defined by the user', () => { - const userDefinedStage = 'myStage'; - const userDefinedStage2 = 'myStage2'; - - const expectedData = { - stages: [ + }, + { + name: job1.stage, + groups: [ { - name: userDefinedStage, - groups: [], + name: jobName1, + jobs: [{ ...job1 }], + id: createUniqueJobId(job1.stage, jobName1), }, { - name: userDefinedStage2, - groups: [], + name: jobName2, + jobs: [{ ...job2 }], + id: createUniqueJobId(job2.stage, jobName2), }, ], - }; + }, + { + name: job3.stage, + groups: [ + { + name: jobName3, + jobs: [{ ...job3 }], + id: createUniqueJobId(job3.stage, jobName3), + }, + ], + }, + ], + jobs: { + [jobName1]: { ...job1, id: createUniqueJobId(job1.stage, jobName1) }, + [jobName2]: { ...job2, id: createUniqueJobId(job2.stage, jobName2) }, + [jobName3]: { ...job3, id: createUniqueJobId(job3.stage, jobName3) }, + [jobName4]: { ...job4, id: createUniqueJobId(job4.stage, jobName4) }, + }, + }; - expect(preparePipelineGraphData({ stages: [userDefinedStage, userDefinedStage2] })).toEqual( - expectedData, - ); - }); + describe('preparePipelineGraphData', () => { + describe('returns an empty array of stages and empty job objects if', () => { + it('no data is passed', () => { + expect(preparePipelineGraphData({})).toEqual(emptyResponse); + }); - it('by combining user defined stage and job stages, it preserves user defined order', () => { - const userDefinedStage = 'myStage'; - const userDefinedStageThatOverlaps = 'deploy'; + it('no stages are found', () => { + expect(preparePipelineGraphData({ includes: 'template/myTemplate.gitlab-ci.yml' })).toEqual( + emptyResponse, + ); + }); + }); - const expectedData = { - stages: [ - { - name: userDefinedStage, - groups: [], - }, - { - name: job4[jobName4].stage, - groups: [ - { - name: jobName4, - jobs: [{ script: job4[jobName4].script, stage: job4[jobName4].stage }], - }, - ], - }, - { - name: job1[jobName1].stage, - groups: [ - { - name: jobName1, - jobs: [{ script: job1[jobName1].script, stage: job1[jobName1].stage }], - }, - { - name: jobName2, - jobs: [{ script: job2[jobName2].script, stage: job2[jobName2].stage }], - }, - ], + describe('returns the correct array of stages and object of jobs', () => { + it('when multiple jobs are in the same stage', () => { + const expectedData = { + stages: [ + { + name: job1.stage, + groups: [ + { + name: jobName1, + jobs: [{ ...job1 }], + id: createUniqueJobId(job1.stage, jobName1), + }, + { + name: jobName2, + jobs: [{ ...job2 }], + id: createUniqueJobId(job2.stage, jobName2), + }, + ], + }, + ], + jobs: { + [jobName1]: { ...job1, id: createUniqueJobId(job1.stage, jobName1) }, + [jobName2]: { ...job2, id: createUniqueJobId(job2.stage, jobName2) }, }, - { - name: job3[jobName3].stage, - groups: [ - { - name: jobName3, - jobs: [{ script: job3[jobName3].script, stage: job3[jobName3].stage }], - }, - ], + }; + expect( + preparePipelineGraphData({ [jobName1]: { ...job1 }, [jobName2]: { ...job2 } }), + ).toEqual(expectedData); + }); + + it('when stages are defined by the user', () => { + const userDefinedStage2 = 'myStage2'; + + const expectedData = { + stages: [ + { + name: userDefinedStage, + groups: [], + }, + { + name: userDefinedStage2, + groups: [], + }, + ], + jobs: {}, + }; + + expect(preparePipelineGraphData({ stages: [userDefinedStage, userDefinedStage2] })).toEqual( + expectedData, + ); + }); + + it('by combining user defined stage and job stages, it preserves user defined order', () => { + const userDefinedStageThatOverlaps = 'deploy'; + + expect( + preparePipelineGraphData({ + stages: [userDefinedStage, userDefinedStageThatOverlaps], + [jobName1]: { ...job1 }, + [jobName2]: { ...job2 }, + [jobName3]: { ...job3 }, + [jobName4]: { ...job4 }, + }), + ).toEqual(pipelineGraphData); + }); + + it('with only unique values', () => { + const expectedData = { + stages: [ + { + name: job1.stage, + groups: [ + { + name: jobName1, + jobs: [{ ...job1 }], + id: createUniqueJobId(job1.stage, jobName1), + }, + ], + }, + ], + jobs: { + [jobName1]: { ...job1, id: createUniqueJobId(job1.stage, jobName1) }, }, - ], - }; + }; - expect( - preparePipelineGraphData({ - stages: [userDefinedStage, userDefinedStageThatOverlaps], - ...job1, - ...job2, - ...job3, - ...job4, - }), - ).toEqual(expectedData); + expect( + preparePipelineGraphData({ + stages: ['build'], + [jobName1]: { ...job1 }, + [jobName1]: { ...job1 }, + }), + ).toEqual(expectedData); + }); }); + }); - it('with only unique values', () => { - const expectedData = { - stages: [ - { - name: job1[jobName1].stage, - groups: [ - { - name: jobName1, - jobs: [{ script: job1[jobName1].script, stage: job1[jobName1].stage }], - }, - ], - }, - ], + describe('generateJobNeedsDict', () => { + it('generates an empty object if it receives no jobs', () => { + expect(generateJobNeedsDict({ jobs: {} })).toEqual({}); + }); + + it('generates a dict with empty needs if there are no dependencies', () => { + const smallGraph = { + jobs: { + [jobName1]: { ...job1, id: createUniqueJobId(job1.stage, jobName1) }, + [jobName2]: { ...job2, id: createUniqueJobId(job2.stage, jobName2) }, + }, }; - expect( - preparePipelineGraphData({ - stages: ['build'], - ...job1, - ...job1, - }), - ).toEqual(expectedData); + expect(generateJobNeedsDict(smallGraph)).toEqual({ + [pipelineGraphData.jobs[jobName1].id]: [], + [pipelineGraphData.jobs[jobName2].id]: [], + }); + }); + + it('generates a dict where key is the a job and its value is an array of all its needs', () => { + const uniqueJobName1 = pipelineGraphData.jobs[jobName1].id; + const uniqueJobName2 = pipelineGraphData.jobs[jobName2].id; + const uniqueJobName3 = pipelineGraphData.jobs[jobName3].id; + const uniqueJobName4 = pipelineGraphData.jobs[jobName4].id; + + expect(generateJobNeedsDict(pipelineGraphData)).toEqual({ + [uniqueJobName1]: [], + [uniqueJobName2]: [], + [uniqueJobName3]: [uniqueJobName1, uniqueJobName2], + [uniqueJobName4]: [uniqueJobName3, uniqueJobName1, uniqueJobName2], + }); }); }); }); diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js index b0ad6bbd228..1298a2a1524 100644 --- a/spec/frontend/pipelines/pipelines_spec.js +++ b/spec/frontend/pipelines/pipelines_spec.js @@ -1,9 +1,17 @@ import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import waitForPromises from 'helpers/wait_for_promises'; -import { GlFilteredSearch } from '@gitlab/ui'; +import { GlFilteredSearch, GlButton, GlLoadingIcon } from '@gitlab/ui'; import Api from '~/api'; import axios from '~/lib/utils/axios_utils'; +import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue'; +import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; + +import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue'; +import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue'; +import BlankState from '~/pipelines/components/pipelines_list/blank_state.vue'; +import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue'; + import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue'; import Store from '~/pipelines/stores/pipelines_store'; import { pipelineWithStages, stageReply, users, mockSearch, branches } from './mock_data'; @@ -49,6 +57,20 @@ describe('Pipelines', () => { }; const findFilteredSearch = () => wrapper.find(GlFilteredSearch); + const findByTestId = id => wrapper.find(`[data-testid="${id}"]`); + const findNavigationTabs = () => wrapper.find(NavigationTabs); + const findNavigationControls = () => wrapper.find(NavigationControls); + const findTab = tab => findByTestId(`pipelines-tab-${tab}`); + + const findRunPipelineButton = () => findByTestId('run-pipeline-button'); + const findCiLintButton = () => findByTestId('ci-lint-button'); + const findCleanCacheButton = () => findByTestId('clear-cache-button'); + + const findEmptyState = () => wrapper.find(EmptyState); + const findBlankState = () => wrapper.find(BlankState); + const findStagesDropdown = () => wrapper.find('.js-builds-dropdown-button'); + + const findTablePagination = () => wrapper.find(TablePagination); const createComponent = (props = defaultProps, methods) => { wrapper = mount(PipelinesComponent, { @@ -87,19 +109,19 @@ describe('Pipelines', () => { }); it('renders tabs', () => { - expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + expect(findTab('all').text()).toContain('All'); }); it('renders Run Pipeline link', () => { - expect(wrapper.find('.js-run-pipeline').attributes('href')).toBe(paths.newPipelinePath); + expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath); }); it('renders CI Lint link', () => { - expect(wrapper.find('.js-ci-lint').attributes('href')).toBe(paths.ciLintPath); + expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath); }); it('renders Clear Runner Cache button', () => { - expect(wrapper.find('.js-clear-cache').text()).toBe('Clear Runner Caches'); + expect(findCleanCacheButton().text()).toBe('Clear Runner Caches'); }); it('renders pipelines table', () => { @@ -127,23 +149,31 @@ describe('Pipelines', () => { }); it('renders tabs', () => { - expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + expect(findTab('all').text()).toContain('All'); }); it('renders Run Pipeline link', () => { - expect(wrapper.find('.js-run-pipeline').attributes('href')).toEqual(paths.newPipelinePath); + expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath); }); it('renders CI Lint link', () => { - expect(wrapper.find('.js-ci-lint').attributes('href')).toEqual(paths.ciLintPath); + expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath); }); it('renders Clear Runner Cache button', () => { - expect(wrapper.find('.js-clear-cache').text()).toEqual('Clear Runner Caches'); + expect(findCleanCacheButton().text()).toBe('Clear Runner Caches'); }); it('renders tab empty state', () => { - expect(wrapper.find('.empty-state h4').text()).toEqual('There are currently no pipelines.'); + expect(findBlankState().text()).toBe('There are currently no pipelines.'); + }); + + it('renders tab empty state finished scope', () => { + wrapper.vm.scope = 'finished'; + + return wrapper.vm.$nextTick().then(() => { + expect(findBlankState().text()).toBe('There are currently no finished pipelines.'); + }); }); }); @@ -165,18 +195,23 @@ describe('Pipelines', () => { }); it('renders empty state', () => { - expect(wrapper.find('.js-empty-state h4').text()).toEqual('Build with confidence'); - - expect(wrapper.find('.js-get-started-pipelines').attributes('href')).toEqual( - paths.helpPagePath, - ); + expect( + findEmptyState() + .find('h4') + .text(), + ).toBe('Build with confidence'); + expect( + findEmptyState() + .find(GlButton) + .attributes('href'), + ).toBe(paths.helpPagePath); }); it('does not render tabs nor buttons', () => { - expect(wrapper.find('.js-pipelines-tab-all').exists()).toBeFalsy(); - expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); - expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); - expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + expect(findTab('all').exists()).toBe(false); + expect(findRunPipelineButton().exists()).toBeFalsy(); + expect(findCiLintButton().exists()).toBeFalsy(); + expect(findCleanCacheButton().exists()).toBeFalsy(); }); }); @@ -189,20 +224,18 @@ describe('Pipelines', () => { }); it('renders tabs', () => { - expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + expect(findTab('all').text()).toContain('All'); }); it('renders buttons', () => { - expect(wrapper.find('.js-run-pipeline').attributes('href')).toEqual(paths.newPipelinePath); + expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath); - expect(wrapper.find('.js-ci-lint').attributes('href')).toEqual(paths.ciLintPath); - expect(wrapper.find('.js-clear-cache').text()).toEqual('Clear Runner Caches'); + expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath); + expect(findCleanCacheButton().text()).toBe('Clear Runner Caches'); }); it('renders error state', () => { - expect(wrapper.find('.empty-state').text()).toContain( - 'There was an error fetching the pipelines.', - ); + expect(findBlankState().text()).toContain('There was an error fetching the pipelines.'); }); }); }); @@ -218,13 +251,13 @@ describe('Pipelines', () => { }); it('renders tabs', () => { - expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + expect(findTab('all').text()).toContain('All'); }); it('does not render buttons', () => { - expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); - expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); - expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + expect(findRunPipelineButton().exists()).toBeFalsy(); + expect(findCiLintButton().exists()).toBeFalsy(); + expect(findCleanCacheButton().exists()).toBeFalsy(); }); it('renders pipelines table', () => { @@ -252,17 +285,17 @@ describe('Pipelines', () => { }); it('renders tabs', () => { - expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + expect(findTab('all').text()).toContain('All'); }); it('does not render buttons', () => { - expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); - expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); - expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + expect(findRunPipelineButton().exists()).toBeFalsy(); + expect(findCiLintButton().exists()).toBeFalsy(); + expect(findCleanCacheButton().exists()).toBeFalsy(); }); it('renders tab empty state', () => { - expect(wrapper.find('.empty-state h4').text()).toEqual('There are currently no pipelines.'); + expect(wrapper.find('.empty-state h4').text()).toBe('There are currently no pipelines.'); }); }); @@ -284,18 +317,22 @@ describe('Pipelines', () => { }); it('renders empty state without button to set CI', () => { - expect(wrapper.find('.js-empty-state').text()).toEqual( + expect(findEmptyState().text()).toBe( 'This project is not currently set up to run pipelines.', ); - expect(wrapper.find('.js-get-started-pipelines').exists()).toBeFalsy(); + expect( + findEmptyState() + .find(GlButton) + .exists(), + ).toBeFalsy(); }); it('does not render tabs or buttons', () => { - expect(wrapper.find('.js-pipelines-tab-all').exists()).toBeFalsy(); - expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); - expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); - expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + expect(findTab('all').exists()).toBe(false); + expect(findRunPipelineButton().exists()).toBeFalsy(); + expect(findCiLintButton().exists()).toBeFalsy(); + expect(findCleanCacheButton().exists()).toBeFalsy(); }); }); @@ -309,13 +346,13 @@ describe('Pipelines', () => { }); it('renders tabs', () => { - expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + expect(findTab('all').text()).toContain('All'); }); it('does not renders buttons', () => { - expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); - expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); - expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + expect(findRunPipelineButton().exists()).toBeFalsy(); + expect(findCiLintButton().exists()).toBeFalsy(); + expect(findCleanCacheButton().exists()).toBeFalsy(); }); it('renders error state', () => { @@ -342,14 +379,20 @@ describe('Pipelines', () => { ); }); - it('should render navigation tabs', () => { - expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); - - expect(wrapper.find('.js-pipelines-tab-finished').text()).toContain('Finished'); - - expect(wrapper.find('.js-pipelines-tab-branches').text()).toContain('Branches'); + it('should set up navigation tabs', () => { + expect(findNavigationTabs().props('tabs')).toEqual([ + { name: 'All', scope: 'all', count: '3', isActive: true }, + { name: 'Finished', scope: 'finished', count: undefined, isActive: false }, + { name: 'Branches', scope: 'branches', isActive: false }, + { name: 'Tags', scope: 'tags', isActive: false }, + ]); + }); - expect(wrapper.find('.js-pipelines-tab-tags').text()).toContain('Tags'); + it('should render navigation tabs', () => { + expect(findTab('all').html()).toContain('All'); + expect(findTab('finished').text()).toContain('Finished'); + expect(findTab('branches').text()).toContain('Branches'); + expect(findTab('tags').text()).toContain('Tags'); }); it('should make an API request when using tabs', () => { @@ -362,7 +405,7 @@ describe('Pipelines', () => { ); return waitForPromises().then(() => { - wrapper.find('.js-pipelines-tab-finished').trigger('click'); + findTab('finished').trigger('click'); expect(updateContentMock).toHaveBeenCalledWith({ scope: 'finished', page: '1' }); }); @@ -401,133 +444,172 @@ describe('Pipelines', () => { }); }); - describe('methods', () => { + describe('User Interaction', () => { + let updateContentMock; + beforeEach(() => { jest.spyOn(window.history, 'pushState').mockImplementation(() => null); }); - describe('onChangeTab', () => { - it('should set page to 1', () => { - const updateContentMock = jest.fn(() => {}); - createComponent( - { hasGitlabCi: true, canCreatePipeline: true, ...paths }, - { - updateContent: updateContentMock, - }, - ); + beforeEach(() => { + mock.onGet(paths.endpoint).reply(200, pipelines); + createComponent(); - wrapper.vm.onChangeTab('running'); + updateContentMock = jest.spyOn(wrapper.vm, 'updateContent'); + + return waitForPromises(); + }); + + describe('when user changes tabs', () => { + it('should set page to 1', () => { + findNavigationTabs().vm.$emit('onChangeTab', 'running'); expect(updateContentMock).toHaveBeenCalledWith({ scope: 'running', page: '1' }); }); }); - describe('onChangePage', () => { + describe('when user changes page', () => { it('should update page and keep scope', () => { - const updateContentMock = jest.fn(() => {}); - createComponent( - { hasGitlabCi: true, canCreatePipeline: true, ...paths }, - { - updateContent: updateContentMock, - }, - ); - - wrapper.vm.onChangePage(4); + findTablePagination().vm.change(4); expect(updateContentMock).toHaveBeenCalledWith({ scope: wrapper.vm.scope, page: '4' }); }); }); - }); - describe('computed properties', () => { - beforeEach(() => { - createComponent(); - }); + describe('updates results when a staged is clicked', () => { + beforeEach(() => { + const copyPipeline = { ...pipelineWithStages }; + copyPipeline.id += 1; + mock + .onGet('twitter/flight/pipelines.json') + .reply( + 200, + { + pipelines: [pipelineWithStages], + count: { + all: 1, + finished: 1, + pending: 0, + running: 0, + }, + }, + { + 'POLL-INTERVAL': 100, + }, + ) + .onGet(pipelineWithStages.details.stages[0].dropdown_path) + .reply(200, stageReply); - describe('tabs', () => { - it('returns default tabs', () => { - expect(wrapper.vm.tabs).toEqual([ - { name: 'All', scope: 'all', count: undefined, isActive: true }, - { name: 'Finished', scope: 'finished', count: undefined, isActive: false }, - { name: 'Branches', scope: 'branches', isActive: false }, - { name: 'Tags', scope: 'tags', isActive: false }, - ]); + createComponent(); }); - }); - describe('emptyTabMessage', () => { - it('returns message with finished scope', () => { - wrapper.vm.scope = 'finished'; + describe('when a request is being made', () => { + it('stops polling, cancels the request, & restarts polling', () => { + const stopMock = jest.spyOn(wrapper.vm.poll, 'stop'); + const restartMock = jest.spyOn(wrapper.vm.poll, 'restart'); + const cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel'); + mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.emptyTabMessage).toEqual('There are currently no finished pipelines.'); + return waitForPromises() + .then(() => { + wrapper.vm.isMakingRequest = true; + findStagesDropdown().trigger('click'); + }) + .then(() => { + expect(cancelMock).toHaveBeenCalled(); + expect(stopMock).toHaveBeenCalled(); + expect(restartMock).toHaveBeenCalled(); + }); }); }); - it('returns message without scope when scope is `all`', () => { - expect(wrapper.vm.emptyTabMessage).toEqual('There are currently no pipelines.'); + describe('when no request is being made', () => { + it('stops polling & restarts polling', () => { + const stopMock = jest.spyOn(wrapper.vm.poll, 'stop'); + const restartMock = jest.spyOn(wrapper.vm.poll, 'restart'); + mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); + + return waitForPromises() + .then(() => { + findStagesDropdown().trigger('click'); + expect(stopMock).toHaveBeenCalled(); + }) + .then(() => { + expect(restartMock).toHaveBeenCalled(); + }); + }); }); }); + }); - describe('stateToRender', () => { - it('returns loading state when the app is loading', () => { - expect(wrapper.vm.stateToRender).toEqual('loading'); + describe('Rendered content', () => { + beforeEach(() => { + createComponent(); + }); + + describe('displays different content', () => { + it('shows loading state when the app is loading', () => { + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); }); - it('returns error state when app has error', () => { + it('shows error state when app has error', () => { wrapper.vm.hasError = true; wrapper.vm.isLoading = false; return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.stateToRender).toEqual('error'); + expect(findBlankState().props('message')).toBe( + 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.', + ); }); }); - it('returns table list when app has pipelines', () => { + it('shows table list when app has pipelines', () => { wrapper.vm.isLoading = false; wrapper.vm.hasError = false; wrapper.vm.state.pipelines = pipelines.pipelines; return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.stateToRender).toEqual('tableList'); + expect(wrapper.find(PipelinesTableComponent).exists()).toBe(true); }); }); - it('returns empty tab when app does not have pipelines but project has pipelines', () => { + it('shows empty tab when app does not have pipelines but project has pipelines', () => { wrapper.vm.state.count.all = 10; wrapper.vm.isLoading = false; return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.stateToRender).toEqual('emptyTab'); + expect(findBlankState().exists()).toBe(true); + expect(findBlankState().props('message')).toBe('There are currently no pipelines.'); }); }); - it('returns empty tab when project has CI', () => { + it('shows empty tab when project has CI', () => { wrapper.vm.isLoading = false; return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.stateToRender).toEqual('emptyTab'); + expect(findBlankState().exists()).toBe(true); + expect(findBlankState().props('message')).toBe('There are currently no pipelines.'); }); }); - it('returns empty state when project does not have pipelines nor CI', () => { + it('shows empty state when project does not have pipelines nor CI', () => { createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); wrapper.vm.isLoading = false; return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.stateToRender).toEqual('emptyState'); + expect(wrapper.find(EmptyState).exists()).toBe(true); }); }); }); - describe('shouldRenderTabs', () => { + describe('displays tabs', () => { it('returns true when state is loading & has already made the first request', () => { wrapper.vm.isLoading = true; wrapper.vm.hasMadeRequest = true; return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.shouldRenderTabs).toEqual(true); + expect(findNavigationTabs().exists()).toBe(true); }); }); @@ -537,7 +619,7 @@ describe('Pipelines', () => { wrapper.vm.hasMadeRequest = true; return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.shouldRenderTabs).toEqual(true); + expect(findNavigationTabs().exists()).toBe(true); }); }); @@ -547,7 +629,7 @@ describe('Pipelines', () => { wrapper.vm.hasMadeRequest = true; return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.shouldRenderTabs).toEqual(true); + expect(findNavigationTabs().exists()).toBe(true); }); }); @@ -557,7 +639,7 @@ describe('Pipelines', () => { wrapper.vm.hasMadeRequest = true; return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.shouldRenderTabs).toEqual(true); + expect(findNavigationTabs().exists()).toBe(true); }); }); @@ -565,7 +647,7 @@ describe('Pipelines', () => { wrapper.vm.hasMadeRequest = false; return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.shouldRenderTabs).toEqual(false); + expect(findNavigationTabs().exists()).toBe(false); }); }); @@ -576,17 +658,17 @@ describe('Pipelines', () => { wrapper.vm.hasMadeRequest = true; return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.shouldRenderTabs).toEqual(false); + expect(findNavigationTabs().exists()).toBe(false); }); }); }); - describe('shouldRenderButtons', () => { + describe('displays buttons', () => { it('returns true when it has paths & has made the first request', () => { wrapper.vm.hasMadeRequest = true; return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.shouldRenderButtons).toEqual(true); + expect(findNavigationControls().exists()).toBe(true); }); }); @@ -594,77 +676,12 @@ describe('Pipelines', () => { wrapper.vm.hasMadeRequest = false; return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.shouldRenderButtons).toEqual(false); + expect(findNavigationControls().exists()).toBe(false); }); }); }); }); - describe('updates results when a staged is clicked', () => { - beforeEach(() => { - const copyPipeline = { ...pipelineWithStages }; - copyPipeline.id += 1; - mock - .onGet('twitter/flight/pipelines.json') - .reply( - 200, - { - pipelines: [pipelineWithStages], - count: { - all: 1, - finished: 1, - pending: 0, - running: 0, - }, - }, - { - 'POLL-INTERVAL': 100, - }, - ) - .onGet(pipelineWithStages.details.stages[0].dropdown_path) - .reply(200, stageReply); - - createComponent(); - }); - - describe('when a request is being made', () => { - it('stops polling, cancels the request, & restarts polling', () => { - const stopMock = jest.spyOn(wrapper.vm.poll, 'stop'); - const restartMock = jest.spyOn(wrapper.vm.poll, 'restart'); - const cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel'); - mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); - - return waitForPromises() - .then(() => { - wrapper.vm.isMakingRequest = true; - wrapper.find('.js-builds-dropdown-button').trigger('click'); - }) - .then(() => { - expect(cancelMock).toHaveBeenCalled(); - expect(stopMock).toHaveBeenCalled(); - expect(restartMock).toHaveBeenCalled(); - }); - }); - }); - - describe('when no request is being made', () => { - it('stops polling & restarts polling', () => { - const stopMock = jest.spyOn(wrapper.vm.poll, 'stop'); - const restartMock = jest.spyOn(wrapper.vm.poll, 'restart'); - mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); - - return waitForPromises() - .then(() => { - wrapper.find('.js-builds-dropdown-button').trigger('click'); - expect(stopMock).toHaveBeenCalled(); - }) - .then(() => { - expect(restartMock).toHaveBeenCalled(); - }); - }); - }); - }); - describe('Pipeline filters', () => { let updateContentMock; diff --git a/spec/frontend/pipelines/pipelines_table_row_spec.js b/spec/frontend/pipelines/pipelines_table_row_spec.js index 9901f476f1b..32d53c0f1f8 100644 --- a/spec/frontend/pipelines/pipelines_table_row_spec.js +++ b/spec/frontend/pipelines/pipelines_table_row_spec.js @@ -181,7 +181,9 @@ describe('Pipelines Table Row', () => { it('should render the provided actions', () => { expect(wrapper.find('.js-pipelines-retry-button').exists()).toBe(true); + expect(wrapper.find('.js-pipelines-retry-button').attributes('title')).toMatch('Retry'); expect(wrapper.find('.js-pipelines-cancel-button').exists()).toBe(true); + expect(wrapper.find('.js-pipelines-cancel-button').attributes('title')).toMatch('Cancel'); const dropdownMenu = wrapper.find('.dropdown-menu'); expect(dropdownMenu.text()).toContain(scheduledJobAction.name); diff --git a/spec/frontend/pipelines/test_reports/mock_data.js b/spec/frontend/pipelines/test_reports/mock_data.js index 1d03f0b655f..c3ca1429842 100644 --- a/spec/frontend/pipelines/test_reports/mock_data.js +++ b/spec/frontend/pipelines/test_reports/mock_data.js @@ -3,10 +3,29 @@ import { TestStatus } from '~/pipelines/constants'; export default [ { classname: 'spec.test_spec', + file: 'spec/trace_spec.rb', execution_time: 0, name: 'Test#skipped text', stack_trace: null, status: TestStatus.SKIPPED, system_output: null, }, + { + classname: 'spec.test_spec', + file: 'spec/trace_spec.rb', + execution_time: 0, + name: 'Test#error text', + stack_trace: null, + status: TestStatus.ERROR, + system_output: null, + }, + { + classname: 'spec.test_spec', + file: 'spec/trace_spec.rb', + execution_time: 0, + name: 'Test#unknown text', + stack_trace: null, + status: TestStatus.UNKNOWN, + system_output: null, + }, ]; 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 2feb6aa5799..838e0606375 100644 --- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js +++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js @@ -1,6 +1,7 @@ import Vuex from 'vuex'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import { getJSONFixture } from 'helpers/fixtures'; +import { GlButton } from '@gitlab/ui'; import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue'; import * as getters from '~/pipelines/stores/test_reports/getters'; import { TestStatus } from '~/pipelines/constants'; @@ -61,18 +62,27 @@ describe('Test reports suite table', () => { expect(allCaseRows().length).toBe(testCases.length); }); - it('renders the correct icon for each status', () => { - const failedTest = testCases.findIndex(x => x.status === TestStatus.FAILED); - const skippedTest = testCases.findIndex(x => x.status === TestStatus.SKIPPED); - const successTest = testCases.findIndex(x => x.status === TestStatus.SUCCESS); + it.each([ + TestStatus.ERROR, + TestStatus.FAILED, + TestStatus.SKIPPED, + TestStatus.SUCCESS, + 'unknown', + ])('renders the correct icon for test case with %s status', status => { + const test = testCases.findIndex(x => x.status === status); + const row = findCaseRowAtIndex(test); - const failedRow = findCaseRowAtIndex(failedTest); - const skippedRow = findCaseRowAtIndex(skippedTest); - const successRow = findCaseRowAtIndex(successTest); + expect(findIconForRow(row, status).exists()).toBe(true); + }); + + it('renders the file name for the test with a copy button', () => { + const { file } = testCases[0]; + const row = findCaseRowAtIndex(0); + const button = row.find(GlButton); - expect(findIconForRow(failedRow, TestStatus.FAILED).exists()).toBe(true); - expect(findIconForRow(skippedRow, TestStatus.SKIPPED).exists()).toBe(true); - expect(findIconForRow(successRow, TestStatus.SUCCESS).exists()).toBe(true); + expect(row.text()).toContain(file); + expect(button.exists()).toBe(true); + expect(button.attributes('data-clipboard-text')).toBe(file); }); }); }); |