353 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			353 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| import Vue, { nextTick } from 'vue';
 | |
| import VueApollo from 'vue-apollo';
 | |
| import { GlSkeletonLoader, GlIcon, GlLink, GlSprintf, GlButton, GlLoadingIcon } from '@gitlab/ui';
 | |
| import createMockApollo from 'helpers/mock_apollo_helper';
 | |
| import waitForPromises from 'helpers/wait_for_promises';
 | |
| import { stubComponent } from 'helpers/stub_component';
 | |
| import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 | |
| import { createAlert } from '~/alert';
 | |
| 
 | |
| import ForkInfo, { i18n } from '~/repository/components/fork_info.vue';
 | |
| import ConflictsModal from '~/repository/components/fork_sync_conflicts_modal.vue';
 | |
| import forkDetailsQuery from '~/repository/queries/fork_details.query.graphql';
 | |
| import syncForkMutation from '~/repository/mutations/sync_fork.mutation.graphql';
 | |
| import eventHub from '~/repository/event_hub';
 | |
| import {
 | |
|   POLLING_INTERVAL_DEFAULT,
 | |
|   POLLING_INTERVAL_BACKOFF,
 | |
|   FORK_UPDATED_EVENT,
 | |
| } from '~/repository/constants';
 | |
| import { propsForkInfo } from '../mock_data';
 | |
| 
 | |
| jest.mock('~/alert');
 | |
| 
 | |
| describe('ForkInfo component', () => {
 | |
|   let wrapper;
 | |
|   let mockForkDetailsQuery;
 | |
|   const forkInfoError = new Error('Something went wrong');
 | |
|   const projectId = 'gid://gitlab/Project/1';
 | |
|   const showMock = jest.fn();
 | |
|   const synchronizeFork = true;
 | |
| 
 | |
|   Vue.use(VueApollo);
 | |
| 
 | |
|   const waitForPolling = async (interval = POLLING_INTERVAL_DEFAULT) => {
 | |
|     jest.advanceTimersByTime(interval);
 | |
|     await waitForPromises();
 | |
|   };
 | |
| 
 | |
|   const mockResolvedForkDetailsQuery = (
 | |
|     forkDetails = { ahead: 3, behind: 7, isSyncing: false, hasConflicts: false },
 | |
|   ) => {
 | |
|     mockForkDetailsQuery.mockResolvedValue({
 | |
|       data: {
 | |
|         project: { id: projectId, forkDetails },
 | |
|       },
 | |
|     });
 | |
|   };
 | |
| 
 | |
|   const createSyncForkDetailsData = (
 | |
|     forkDetails = { ahead: 3, behind: 7, isSyncing: false, hasConflicts: false },
 | |
|   ) => {
 | |
|     return {
 | |
|       data: {
 | |
|         projectSyncFork: { details: forkDetails, errors: [] },
 | |
|       },
 | |
|     };
 | |
|   };
 | |
| 
 | |
|   const createComponent = (props = {}, mutationData = {}) => {
 | |
|     wrapper = shallowMountExtended(ForkInfo, {
 | |
|       apolloProvider: createMockApollo([
 | |
|         [forkDetailsQuery, mockForkDetailsQuery],
 | |
|         [syncForkMutation, jest.fn().mockResolvedValue(createSyncForkDetailsData(mutationData))],
 | |
|       ]),
 | |
|       propsData: { ...propsForkInfo, ...props },
 | |
|       stubs: {
 | |
|         GlSprintf,
 | |
|         GlButton,
 | |
|         ConflictsModal: stubComponent(ConflictsModal, {
 | |
|           template:
 | |
|             '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
 | |
|           methods: { show: showMock },
 | |
|         }),
 | |
|       },
 | |
|       provide: {
 | |
|         glFeatures: {
 | |
|           synchronizeFork,
 | |
|         },
 | |
|       },
 | |
|     });
 | |
|     return waitForPromises();
 | |
|   };
 | |
| 
 | |
|   const findLink = () => wrapper.findComponent(GlLink);
 | |
|   const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
 | |
|   const findIcon = () => wrapper.findComponent(GlIcon);
 | |
|   const findUpdateForkButton = () => wrapper.findByTestId('update-fork-button');
 | |
|   const findCreateMrButton = () => wrapper.findByTestId('create-mr-button');
 | |
|   const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
 | |
|   const findDivergenceMessage = () => wrapper.findByTestId('divergence-message');
 | |
|   const findInaccessibleMessage = () => wrapper.findByTestId('inaccessible-project');
 | |
|   const findCompareLinks = () => findDivergenceMessage().findAllComponents(GlLink);
 | |
| 
 | |
|   const startForkUpdate = async () => {
 | |
|     findUpdateForkButton().vm.$emit('click');
 | |
|     await waitForPromises();
 | |
|   };
 | |
| 
 | |
|   beforeEach(() => {
 | |
|     mockForkDetailsQuery = jest.fn();
 | |
|     mockResolvedForkDetailsQuery();
 | |
|   });
 | |
| 
 | |
|   it('displays a skeleton while loading data', async () => {
 | |
|     createComponent();
 | |
|     expect(findSkeleton().exists()).toBe(true);
 | |
|   });
 | |
| 
 | |
|   it('does not display skeleton when data is loaded', async () => {
 | |
|     await createComponent();
 | |
|     expect(findSkeleton().exists()).toBe(false);
 | |
|   });
 | |
| 
 | |
|   it('renders fork icon', async () => {
 | |
|     await createComponent();
 | |
|     expect(findIcon().exists()).toBe(true);
 | |
|   });
 | |
| 
 | |
|   it('queries the data when sourceName is present', async () => {
 | |
|     await createComponent();
 | |
|     expect(mockForkDetailsQuery).toHaveBeenCalled();
 | |
|   });
 | |
| 
 | |
|   it('does not query the data when sourceName is empty', async () => {
 | |
|     await createComponent({ sourceName: null });
 | |
|     expect(mockForkDetailsQuery).not.toHaveBeenCalled();
 | |
|   });
 | |
| 
 | |
|   it('renders inaccessible message when fork source is not available', async () => {
 | |
|     await createComponent({ sourceName: '' });
 | |
|     const message = findInaccessibleMessage();
 | |
|     expect(message.exists()).toBe(true);
 | |
|     expect(message.text()).toBe(i18n.inaccessibleProject);
 | |
|   });
 | |
| 
 | |
|   it('shows source project name with a link to a repo', async () => {
 | |
|     await createComponent();
 | |
|     const link = findLink();
 | |
|     expect(link.text()).toBe(propsForkInfo.sourceName);
 | |
|     expect(link.attributes('href')).toBe(propsForkInfo.sourcePath);
 | |
|   });
 | |
| 
 | |
|   it('renders Create MR Button with correct path', async () => {
 | |
|     await createComponent();
 | |
|     expect(findCreateMrButton().attributes('href')).toBe(propsForkInfo.createMrPath);
 | |
|   });
 | |
| 
 | |
|   it('does not render create MR button if user had no permission to Create MR in fork', async () => {
 | |
|     await createComponent({ canUserCreateMrInFork: false });
 | |
|     expect(findCreateMrButton().exists()).toBe(false);
 | |
|   });
 | |
| 
 | |
|   it('renders alert with error message when request fails', async () => {
 | |
|     mockForkDetailsQuery.mockRejectedValue(forkInfoError);
 | |
|     await createComponent({});
 | |
|     expect(createAlert).toHaveBeenCalledWith({
 | |
|       message: i18n.error,
 | |
|       captureError: true,
 | |
|       error: forkInfoError,
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('Unknown divergence', () => {
 | |
|     it('renders unknown divergence message when divergence is unknown', async () => {
 | |
|       mockResolvedForkDetailsQuery({
 | |
|         ahead: null,
 | |
|         behind: null,
 | |
|         isSyncing: false,
 | |
|         hasConflicts: false,
 | |
|       });
 | |
|       await createComponent({});
 | |
|       expect(findDivergenceMessage().text()).toBe(i18n.unknown);
 | |
|     });
 | |
| 
 | |
|     it('renders Update Fork button', async () => {
 | |
|       mockResolvedForkDetailsQuery({
 | |
|         ahead: null,
 | |
|         behind: null,
 | |
|         isSyncing: false,
 | |
|         hasConflicts: false,
 | |
|       });
 | |
|       await createComponent({});
 | |
|       expect(findUpdateForkButton().exists()).toBe(true);
 | |
|       expect(findUpdateForkButton().text()).toBe(i18n.updateFork);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('Up to date divergence', () => {
 | |
|     beforeEach(async () => {
 | |
|       mockResolvedForkDetailsQuery({ ahead: 0, behind: 0, isSyncing: false, hasConflicts: false });
 | |
|       await createComponent({}, { ahead: 0, behind: 0, isSyncing: false, hasConflicts: false });
 | |
|     });
 | |
| 
 | |
|     it('renders up to date message when fork is up to date', async () => {
 | |
|       expect(findDivergenceMessage().text()).toBe(i18n.upToDate);
 | |
|     });
 | |
| 
 | |
|     it('does not render Update Fork button', async () => {
 | |
|       expect(findUpdateForkButton().exists()).toBe(false);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('Limited visibility project', () => {
 | |
|     beforeEach(async () => {
 | |
|       mockResolvedForkDetailsQuery(null);
 | |
|       await createComponent({}, null);
 | |
|     });
 | |
| 
 | |
|     it('renders limited visibility messsage when forkDetails are empty', async () => {
 | |
|       expect(findDivergenceMessage().text()).toBe(i18n.limitedVisibility);
 | |
|     });
 | |
| 
 | |
|     it('does not render Update Fork button', async () => {
 | |
|       expect(findUpdateForkButton().exists()).toBe(false);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe.each([
 | |
|     {
 | |
|       ahead: 7,
 | |
|       behind: 3,
 | |
|       message: '3 commits behind, 7 commits ahead of the upstream repository.',
 | |
|       firstLink: propsForkInfo.behindComparePath,
 | |
|       secondLink: propsForkInfo.aheadComparePath,
 | |
|       hasUpdateButton: true,
 | |
|       hasCreateMrButton: true,
 | |
|     },
 | |
|     {
 | |
|       ahead: 7,
 | |
|       behind: 0,
 | |
|       message: '7 commits ahead of the upstream repository.',
 | |
|       firstLink: propsForkInfo.aheadComparePath,
 | |
|       secondLink: '',
 | |
|       hasUpdateButton: false,
 | |
|       hasCreateMrButton: true,
 | |
|     },
 | |
|     {
 | |
|       ahead: 0,
 | |
|       behind: 3,
 | |
|       message: '3 commits behind the upstream repository.',
 | |
|       firstLink: propsForkInfo.behindComparePath,
 | |
|       secondLink: '',
 | |
|       hasUpdateButton: true,
 | |
|       hasCreateMrButton: false,
 | |
|     },
 | |
|   ])(
 | |
|     'renders correct divergence message for ahead: $ahead, behind: $behind divergence commits',
 | |
|     ({ ahead, behind, message, firstLink, secondLink, hasUpdateButton, hasCreateMrButton }) => {
 | |
|       beforeEach(async () => {
 | |
|         mockResolvedForkDetailsQuery({ ahead, behind, isSyncing: false, hasConflicts: false });
 | |
|         await createComponent({});
 | |
|       });
 | |
| 
 | |
|       it('displays correct text', () => {
 | |
|         expect(findDivergenceMessage().text()).toBe(message);
 | |
|       });
 | |
| 
 | |
|       it('adds correct links', () => {
 | |
|         const links = findCompareLinks();
 | |
|         expect(links.at(0).attributes('href')).toBe(firstLink);
 | |
| 
 | |
|         if (secondLink) {
 | |
|           expect(links.at(1).attributes('href')).toBe(secondLink);
 | |
|         }
 | |
|       });
 | |
| 
 | |
|       it('renders Update Fork button when fork is behind', () => {
 | |
|         expect(findUpdateForkButton().exists()).toBe(hasUpdateButton);
 | |
|         if (hasUpdateButton) {
 | |
|           expect(findUpdateForkButton().text()).toBe(i18n.updateFork);
 | |
|         }
 | |
|       });
 | |
| 
 | |
|       it('renders Create Merge Request button when fork is ahead', () => {
 | |
|         expect(findCreateMrButton().exists()).toBe(hasCreateMrButton);
 | |
|         if (hasCreateMrButton) {
 | |
|           expect(findCreateMrButton().text()).toBe(i18n.createMergeRequest);
 | |
|         }
 | |
|       });
 | |
|     },
 | |
|   );
 | |
| 
 | |
|   describe('when sync is not possible due to conflicts', () => {
 | |
|     it('Opens Conflicts Modal', async () => {
 | |
|       mockResolvedForkDetailsQuery({ ahead: 7, behind: 3, isSyncing: false, hasConflicts: true });
 | |
|       await createComponent({});
 | |
|       findUpdateForkButton().vm.$emit('click');
 | |
|       expect(showMock).toHaveBeenCalled();
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('projectSyncFork mutation', () => {
 | |
|     it('changes button to have loading state', async () => {
 | |
|       await createComponent({}, { ahead: 0, behind: 3, isSyncing: true, hasConflicts: false });
 | |
|       mockResolvedForkDetailsQuery({ ahead: 0, behind: 3, isSyncing: false, hasConflicts: false });
 | |
|       expect(findLoadingIcon().exists()).toBe(false);
 | |
|       await startForkUpdate();
 | |
|       expect(findLoadingIcon().exists()).toBe(true);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('polling', () => {
 | |
|     beforeEach(async () => {
 | |
|       await createComponent({}, { ahead: 0, behind: 3, isSyncing: true, hasConflicts: false });
 | |
|       mockResolvedForkDetailsQuery({ ahead: 0, behind: 3, isSyncing: true, hasConflicts: false });
 | |
|     });
 | |
| 
 | |
|     it('fetches data on the initial load', () => {
 | |
|       expect(mockForkDetailsQuery).toHaveBeenCalledTimes(1);
 | |
|     });
 | |
| 
 | |
|     it('starts polling after sync button is clicked', async () => {
 | |
|       await startForkUpdate();
 | |
|       await waitForPolling();
 | |
|       expect(mockForkDetailsQuery).toHaveBeenCalledTimes(2);
 | |
| 
 | |
|       await waitForPolling(POLLING_INTERVAL_DEFAULT * POLLING_INTERVAL_BACKOFF);
 | |
|       expect(mockForkDetailsQuery).toHaveBeenCalledTimes(3);
 | |
|     });
 | |
| 
 | |
|     it('stops polling once sync is finished', async () => {
 | |
|       mockResolvedForkDetailsQuery({ ahead: 0, behind: 0, isSyncing: false, hasConflicts: false });
 | |
|       await startForkUpdate();
 | |
|       await waitForPolling();
 | |
|       expect(mockForkDetailsQuery).toHaveBeenCalledTimes(2);
 | |
|       await waitForPolling(POLLING_INTERVAL_DEFAULT * POLLING_INTERVAL_BACKOFF);
 | |
|       expect(mockForkDetailsQuery).toHaveBeenCalledTimes(2);
 | |
|       await nextTick();
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('once fork is updated', () => {
 | |
|     beforeEach(async () => {
 | |
|       await createComponent({}, { ahead: 0, behind: 3, isSyncing: true, hasConflicts: false });
 | |
|       mockResolvedForkDetailsQuery({ ahead: 0, behind: 0, isSyncing: false, hasConflicts: false });
 | |
|     });
 | |
| 
 | |
|     it('emits fork:update event to eventHub', async () => {
 | |
|       jest.spyOn(eventHub, '$emit').mockImplementation();
 | |
|       await startForkUpdate();
 | |
|       await waitForPolling();
 | |
|       expect(eventHub.$emit).toHaveBeenCalledWith(FORK_UPDATED_EVENT);
 | |
|     });
 | |
| 
 | |
|     it('hides update fork button', async () => {
 | |
|       jest.spyOn(eventHub, '$emit').mockImplementation();
 | |
|       await startForkUpdate();
 | |
|       await waitForPolling();
 | |
|       expect(findUpdateForkButton().exists()).toBe(false);
 | |
|     });
 | |
|   });
 | |
| });
 |