457 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			457 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| import { GlAlert, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
 | |
| import Vue, { nextTick } from 'vue';
 | |
| import VueApollo from 'vue-apollo';
 | |
| import { createMockSubscription } from 'mock-apollo-client';
 | |
| import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 | |
| import createMockApollo from 'helpers/mock_apollo_helper';
 | |
| import waitForPromises from 'helpers/wait_for_promises';
 | |
| import { getIdFromGraphQLId } from '~/graphql_shared/utils';
 | |
| import PipelineHeader from '~/ci/pipeline_details/header/pipeline_header.vue';
 | |
| import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
 | |
| import cancelPipelineMutation from '~/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql';
 | |
| import deletePipelineMutation from '~/ci/pipeline_details/graphql/mutations/delete_pipeline.mutation.graphql';
 | |
| import retryPipelineMutation from '~/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql';
 | |
| import HeaderActions from '~/ci/pipeline_details/header/components/header_actions.vue';
 | |
| import HeaderBadges from '~/ci/pipeline_details/header/components/header_badges.vue';
 | |
| import getPipelineDetailsQuery from '~/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql';
 | |
| import pipelineCiStatusUpdatedSubscription from '~/graphql_shared/subscriptions/pipeline_ci_status_updated.subscription.graphql';
 | |
| 
 | |
| import {
 | |
|   pipelineHeaderSuccess,
 | |
|   pipelineHeaderRunning,
 | |
|   pipelineHeaderRunningWithDuration,
 | |
|   pipelineHeaderFailed,
 | |
|   pipelineRetryMutationResponseSuccess,
 | |
|   pipelineCancelMutationResponseSuccess,
 | |
|   pipelineDeleteMutationResponseSuccess,
 | |
|   pipelineRetryMutationResponseFailed,
 | |
|   pipelineCancelMutationResponseFailed,
 | |
|   pipelineDeleteMutationResponseFailed,
 | |
|   mockPipelineStatusUpdatedResponse,
 | |
| } from '../mock_data';
 | |
| 
 | |
| Vue.use(VueApollo);
 | |
| 
 | |
| describe('Pipeline header', () => {
 | |
|   let wrapper;
 | |
|   let mockedSubscription;
 | |
|   let apolloProvider;
 | |
| 
 | |
|   const successHandler = jest.fn().mockResolvedValue(pipelineHeaderSuccess);
 | |
|   const runningHandler = jest.fn().mockResolvedValue(pipelineHeaderRunning);
 | |
|   const runningHandlerWithDuration = jest.fn().mockResolvedValue(pipelineHeaderRunningWithDuration);
 | |
|   const failedHandler = jest.fn().mockResolvedValue(pipelineHeaderFailed);
 | |
| 
 | |
|   const retryMutationHandlerSuccess = jest
 | |
|     .fn()
 | |
|     .mockResolvedValue(pipelineRetryMutationResponseSuccess);
 | |
|   const cancelMutationHandlerSuccess = jest
 | |
|     .fn()
 | |
|     .mockResolvedValue(pipelineCancelMutationResponseSuccess);
 | |
|   const deleteMutationHandlerSuccess = jest
 | |
|     .fn()
 | |
|     .mockResolvedValue(pipelineDeleteMutationResponseSuccess);
 | |
|   const retryMutationHandlerFailed = jest
 | |
|     .fn()
 | |
|     .mockResolvedValue(pipelineRetryMutationResponseFailed);
 | |
|   const cancelMutationHandlerFailed = jest
 | |
|     .fn()
 | |
|     .mockResolvedValue(pipelineCancelMutationResponseFailed);
 | |
|   const deleteMutationHandlerFailed = jest
 | |
|     .fn()
 | |
|     .mockResolvedValue(pipelineDeleteMutationResponseFailed);
 | |
| 
 | |
|   const findAlert = () => wrapper.findComponent(GlAlert);
 | |
|   const findStatus = () => wrapper.findComponent(CiIcon);
 | |
|   const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
 | |
|   const findBadges = () => wrapper.findComponent(HeaderBadges);
 | |
|   const findHeaderActions = () => wrapper.findComponent(HeaderActions);
 | |
|   const findCreatedTimeAgo = () => wrapper.findByTestId('pipeline-created-time-ago');
 | |
|   const findFinishedTimeAgo = () => wrapper.findByTestId('pipeline-finished-time-ago');
 | |
|   const findFinishedCreatedTimeAgo = () =>
 | |
|     wrapper.findByTestId('pipeline-finished-created-time-ago');
 | |
|   const findPipelineName = () => wrapper.findByTestId('pipeline-name');
 | |
|   const findCommitTitle = () => wrapper.findByTestId('pipeline-commit-title');
 | |
|   const findTotalJobs = () => wrapper.findByTestId('total-jobs');
 | |
|   const findCommitLink = () => wrapper.findByTestId('commit-link');
 | |
|   const findCommitCopyButton = () => wrapper.findByTestId('commit-copy-sha');
 | |
|   const findPipelineRunningText = () => wrapper.findByTestId('pipeline-running-text').text();
 | |
|   const findPipelineRefText = () => wrapper.findByTestId('pipeline-ref-text').text();
 | |
|   const findPipelineUserLink = () => wrapper.findByTestId('pipeline-user-link');
 | |
|   const findPipelineDuration = () => wrapper.findByTestId('pipeline-duration-text');
 | |
| 
 | |
|   const clickActionButton = (action, id) => {
 | |
|     findHeaderActions().vm.$emit(action, id);
 | |
|   };
 | |
| 
 | |
|   const defaultHandlers = [[getPipelineDetailsQuery, successHandler]];
 | |
| 
 | |
|   const defaultProvideOptions = {
 | |
|     identityVerificationRequired: false,
 | |
|     identityVerificationPath: '#',
 | |
|     pipelineIid: 1,
 | |
|     pipelineId: 100,
 | |
|     paths: {
 | |
|       pipelinesPath: '/namespace/my-project/-/pipelines',
 | |
|       fullProject: '/namespace/my-project',
 | |
|     },
 | |
|     glFeatures: {
 | |
|       ciPipelineStatusRealtime: true,
 | |
|     },
 | |
|   };
 | |
| 
 | |
|   const createMockApolloProvider = (handlers) => {
 | |
|     return createMockApollo(handlers);
 | |
|   };
 | |
| 
 | |
|   const createComponent = (handlers = defaultHandlers) => {
 | |
|     mockedSubscription = createMockSubscription();
 | |
|     apolloProvider = createMockApolloProvider(handlers);
 | |
| 
 | |
|     apolloProvider.defaultClient.setRequestHandler(
 | |
|       pipelineCiStatusUpdatedSubscription,
 | |
|       () => mockedSubscription,
 | |
|     );
 | |
| 
 | |
|     wrapper = shallowMountExtended(PipelineHeader, {
 | |
|       provide: defaultProvideOptions,
 | |
|       stubs: { GlSprintf },
 | |
|       apolloProvider,
 | |
|     });
 | |
| 
 | |
|     return waitForPromises();
 | |
|   };
 | |
| 
 | |
|   describe('loading state', () => {
 | |
|     it('shows a loading state while graphQL is fetching initial data', () => {
 | |
|       createComponent();
 | |
| 
 | |
|       expect(findLoadingIcon().exists()).toBe(true);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('defaults', () => {
 | |
|     beforeEach(() => {
 | |
|       return createComponent();
 | |
|     });
 | |
| 
 | |
|     it('does not display loading icon', () => {
 | |
|       expect(findLoadingIcon().exists()).toBe(false);
 | |
|     });
 | |
| 
 | |
|     it('displays pipeline status', () => {
 | |
|       expect(findStatus().exists()).toBe(true);
 | |
|     });
 | |
| 
 | |
|     it('displays pipeline name', () => {
 | |
|       expect(findPipelineName().text()).toBe('Build pipeline');
 | |
|     });
 | |
| 
 | |
|     it('displays total jobs', () => {
 | |
|       expect(findTotalJobs().text()).toBe('3 jobs');
 | |
|     });
 | |
| 
 | |
|     it('has link to commit', () => {
 | |
|       const {
 | |
|         data: {
 | |
|           project: { pipeline },
 | |
|         },
 | |
|       } = pipelineHeaderSuccess;
 | |
| 
 | |
|       expect(findCommitLink().attributes('href')).toBe(pipeline.commit.webPath);
 | |
|       expect(findCommitLink().text()).toBe(pipeline.commit.shortId);
 | |
|     });
 | |
| 
 | |
|     it('copies the full commit ID', () => {
 | |
|       const {
 | |
|         data: {
 | |
|           project: { pipeline },
 | |
|         },
 | |
|       } = pipelineHeaderSuccess;
 | |
| 
 | |
|       expect(findCommitCopyButton().props('text')).toBe(pipeline.commit.sha);
 | |
|     });
 | |
| 
 | |
|     it('displays badges', () => {
 | |
|       expect(findBadges().exists()).toBe(true);
 | |
|     });
 | |
| 
 | |
|     it('passes pipeline prop to HeaderBadges component', () => {
 | |
|       expect(findBadges().props('pipeline')).toEqual(pipelineHeaderSuccess.data.project.pipeline);
 | |
|     });
 | |
| 
 | |
|     it('displays ref text', () => {
 | |
|       expect(findPipelineRefText()).toBe('Related merge request !1 to merge master into feature');
 | |
|     });
 | |
| 
 | |
|     it('displays pipeline user link with required user popover attributes', () => {
 | |
|       const {
 | |
|         data: {
 | |
|           project: {
 | |
|             pipeline: { user },
 | |
|           },
 | |
|         },
 | |
|       } = pipelineHeaderSuccess;
 | |
| 
 | |
|       const userId = getIdFromGraphQLId(user.id).toString();
 | |
| 
 | |
|       expect(findPipelineUserLink().classes()).toContain('js-user-link');
 | |
|       expect(findPipelineUserLink().attributes('data-user-id')).toBe(userId);
 | |
|       expect(findPipelineUserLink().attributes('data-username')).toBe(user.username);
 | |
|       expect(findPipelineUserLink().attributes('href')).toBe(user.webUrl);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('without pipeline name', () => {
 | |
|     it('displays commit title', async () => {
 | |
|       await createComponent([[getPipelineDetailsQuery, runningHandler]]);
 | |
| 
 | |
|       const expectedTitle = pipelineHeaderSuccess.data.project.pipeline.commit.title;
 | |
| 
 | |
|       expect(findPipelineName().exists()).toBe(false);
 | |
|       expect(findCommitTitle().text()).toBe(expectedTitle);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('finished pipeline', () => {
 | |
|     it('displays finished time and created time', async () => {
 | |
|       await createComponent();
 | |
| 
 | |
|       expect(findFinishedTimeAgo().exists()).toBe(true);
 | |
|       expect(findFinishedCreatedTimeAgo().exists()).toBe(true);
 | |
|     });
 | |
| 
 | |
|     it('displays pipeline duartion text', async () => {
 | |
|       await createComponent();
 | |
| 
 | |
|       expect(findPipelineDuration().text()).toBe(
 | |
|         '120 minutes 10 seconds, queued for 3,600 seconds',
 | |
|       );
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('running pipeline', () => {
 | |
|     beforeEach(() => {
 | |
|       return createComponent([[getPipelineDetailsQuery, runningHandler]]);
 | |
|     });
 | |
| 
 | |
|     it('does not display finished time ago', () => {
 | |
|       expect(findFinishedTimeAgo().exists()).toBe(false);
 | |
|       expect(findFinishedCreatedTimeAgo().exists()).toBe(false);
 | |
|     });
 | |
| 
 | |
|     it('does not display pipeline duration text', () => {
 | |
|       expect(findPipelineDuration().exists()).toBe(false);
 | |
|     });
 | |
| 
 | |
|     it('displays pipeline running text', () => {
 | |
|       expect(findPipelineRunningText()).toBe('In progress, queued for 3,600 seconds');
 | |
|     });
 | |
| 
 | |
|     it('displays created time ago', () => {
 | |
|       expect(findCreatedTimeAgo().exists()).toBe(true);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('running pipeline with duration', () => {
 | |
|     beforeEach(() => {
 | |
|       return createComponent([[getPipelineDetailsQuery, runningHandlerWithDuration]]);
 | |
|     });
 | |
| 
 | |
|     it('does not display pipeline duration text', () => {
 | |
|       expect(findPipelineDuration().exists()).toBe(false);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('actions', () => {
 | |
|     it('passes correct props to the header actions component', async () => {
 | |
|       await createComponent([
 | |
|         [getPipelineDetailsQuery, failedHandler],
 | |
|         [retryPipelineMutation, retryMutationHandlerSuccess],
 | |
|       ]);
 | |
| 
 | |
|       expect(findHeaderActions().props()).toEqual({
 | |
|         isCanceling: false,
 | |
|         isDeleting: false,
 | |
|         isRetrying: false,
 | |
|         pipeline: pipelineHeaderFailed.data.project.pipeline,
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('retry action', () => {
 | |
|       beforeEach(() => {
 | |
|         return createComponent([
 | |
|           [getPipelineDetailsQuery, failedHandler],
 | |
|           [retryPipelineMutation, retryMutationHandlerSuccess],
 | |
|         ]);
 | |
|       });
 | |
| 
 | |
|       it('should call retryPipeline Mutation with pipeline id', async () => {
 | |
|         clickActionButton('retryPipeline', pipelineHeaderFailed.data.project.pipeline.id);
 | |
| 
 | |
|         await nextTick();
 | |
| 
 | |
|         expect(findHeaderActions().props('isRetrying')).toBe(true);
 | |
|         expect(retryMutationHandlerSuccess).toHaveBeenCalledWith({
 | |
|           id: pipelineHeaderFailed.data.project.pipeline.id,
 | |
|         });
 | |
|         expect(findAlert().exists()).toBe(false);
 | |
| 
 | |
|         await waitForPromises();
 | |
| 
 | |
|         expect(findHeaderActions().props('isRetrying')).toBe(false);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('retry action failed', () => {
 | |
|       beforeEach(() => {
 | |
|         return createComponent([
 | |
|           [getPipelineDetailsQuery, failedHandler],
 | |
|           [retryPipelineMutation, retryMutationHandlerFailed],
 | |
|         ]);
 | |
|       });
 | |
| 
 | |
|       it('should display error message on failure', async () => {
 | |
|         clickActionButton('retryPipeline', pipelineHeaderFailed.data.project.pipeline.id);
 | |
| 
 | |
|         await waitForPromises();
 | |
| 
 | |
|         expect(findAlert().exists()).toBe(true);
 | |
|       });
 | |
| 
 | |
|       it('retry button loading state should reset on error', async () => {
 | |
|         clickActionButton('retryPipeline', pipelineHeaderFailed.data.project.pipeline.id);
 | |
| 
 | |
|         await nextTick();
 | |
| 
 | |
|         expect(findHeaderActions().props('isRetrying')).toBe(true);
 | |
| 
 | |
|         await waitForPromises();
 | |
| 
 | |
|         expect(findHeaderActions().props('isRetrying')).toBe(false);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('cancel action', () => {
 | |
|       describe('with permissions', () => {
 | |
|         it('should call cancelPipeline Mutation with pipeline id', async () => {
 | |
|           await createComponent([
 | |
|             [getPipelineDetailsQuery, runningHandler],
 | |
|             [cancelPipelineMutation, cancelMutationHandlerSuccess],
 | |
|           ]);
 | |
| 
 | |
|           clickActionButton('cancelPipeline', pipelineHeaderRunning.data.project.pipeline.id);
 | |
| 
 | |
|           await nextTick();
 | |
| 
 | |
|           expect(findHeaderActions().props('isCanceling')).toBe(true);
 | |
|           expect(cancelMutationHandlerSuccess).toHaveBeenCalledWith({
 | |
|             id: pipelineHeaderRunning.data.project.pipeline.id,
 | |
|           });
 | |
|           expect(findAlert().exists()).toBe(false);
 | |
| 
 | |
|           await waitForPromises();
 | |
| 
 | |
|           expect(findHeaderActions().props('isCanceling')).toBe(false);
 | |
|         });
 | |
| 
 | |
|         it('should display error message on failure', async () => {
 | |
|           await createComponent([
 | |
|             [getPipelineDetailsQuery, runningHandler],
 | |
|             [cancelPipelineMutation, cancelMutationHandlerFailed],
 | |
|           ]);
 | |
| 
 | |
|           clickActionButton('cancelPipeline', pipelineHeaderRunning.data.project.pipeline.id);
 | |
| 
 | |
|           await waitForPromises();
 | |
| 
 | |
|           expect(findAlert().exists()).toBe(true);
 | |
|         });
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('delete action', () => {
 | |
|       it('should call deletePipeline Mutation with pipeline id when modal is submitted', async () => {
 | |
|         await createComponent([
 | |
|           [getPipelineDetailsQuery, successHandler],
 | |
|           [deletePipelineMutation, deleteMutationHandlerSuccess],
 | |
|         ]);
 | |
| 
 | |
|         clickActionButton('deletePipeline', pipelineHeaderSuccess.data.project.pipeline.id);
 | |
| 
 | |
|         await nextTick();
 | |
| 
 | |
|         expect(findHeaderActions().props('isDeleting')).toBe(true);
 | |
|         expect(deleteMutationHandlerSuccess).toHaveBeenCalledWith({
 | |
|           id: pipelineHeaderSuccess.data.project.pipeline.id,
 | |
|         });
 | |
| 
 | |
|         await waitForPromises();
 | |
| 
 | |
|         expect(findHeaderActions().props('isDeleting')).toBe(false);
 | |
|       });
 | |
| 
 | |
|       it('should display error message on failure', async () => {
 | |
|         await createComponent([
 | |
|           [getPipelineDetailsQuery, successHandler],
 | |
|           [deletePipelineMutation, deleteMutationHandlerFailed],
 | |
|         ]);
 | |
| 
 | |
|         clickActionButton('deletePipeline', pipelineHeaderSuccess.data.project.pipeline.id);
 | |
| 
 | |
|         await waitForPromises();
 | |
| 
 | |
|         expect(findAlert().exists()).toBe(true);
 | |
|       });
 | |
| 
 | |
|       it('delete button loading state should reset on error', async () => {
 | |
|         await createComponent([
 | |
|           [getPipelineDetailsQuery, successHandler],
 | |
|           [deletePipelineMutation, deleteMutationHandlerFailed],
 | |
|         ]);
 | |
| 
 | |
|         clickActionButton('deletePipeline', pipelineHeaderSuccess.data.project.pipeline.id);
 | |
| 
 | |
|         await nextTick();
 | |
| 
 | |
|         expect(findHeaderActions().props('isDeleting')).toBe(true);
 | |
| 
 | |
|         await waitForPromises();
 | |
| 
 | |
|         expect(findHeaderActions().props('isDeleting')).toBe(false);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('subscription', () => {
 | |
|       it('updates pipeline status when subscription updates', async () => {
 | |
|         await createComponent([
 | |
|           [getPipelineDetailsQuery, runningHandler],
 | |
|           [cancelPipelineMutation, cancelMutationHandlerFailed],
 | |
|         ]);
 | |
| 
 | |
|         const {
 | |
|           data: {
 | |
|             project: {
 | |
|               pipeline: { detailedStatus },
 | |
|             },
 | |
|           },
 | |
|         } = pipelineHeaderRunning;
 | |
| 
 | |
|         expect(findStatus().props('status')).toStrictEqual(detailedStatus);
 | |
| 
 | |
|         mockedSubscription.next(mockPipelineStatusUpdatedResponse);
 | |
| 
 | |
|         await waitForPromises();
 | |
| 
 | |
|         expect(findStatus().props('status')).toStrictEqual({
 | |
|           __typename: 'DetailedStatus',
 | |
|           detailsPath: '/root/simple-ci-project/-/pipelines/1257',
 | |
|           icon: 'status_success',
 | |
|           id: 'success-1255-1255',
 | |
|           text: 'Passed',
 | |
|         });
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| });
 |