388 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			388 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| import { GlAlert } from '@gitlab/ui';
 | |
| import { shallowMount } from '@vue/test-utils';
 | |
| import Vue, { nextTick } from 'vue';
 | |
| import VueRouter from 'vue-router';
 | |
| import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
 | |
| import Api from '~/api';
 | |
| import DesignPresentation from '~/design_management/components/design_presentation.vue';
 | |
| import DesignSidebar from '~/design_management/components/design_sidebar.vue';
 | |
| import DesignDestroyer from '~/design_management/components/design_destroyer.vue';
 | |
| import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants';
 | |
| import updateActiveDiscussion from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
 | |
| import getDesignQuery from '~/design_management/graphql/queries/get_design.query.graphql';
 | |
| import DesignIndex from '~/design_management/pages/design/index.vue';
 | |
| import createRouter from '~/design_management/router';
 | |
| import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from '~/design_management/router/constants';
 | |
| import * as utils from '~/design_management/utils/design_management_utils';
 | |
| import {
 | |
|   DESIGN_NOT_FOUND_ERROR,
 | |
|   DESIGN_VERSION_NOT_EXIST_ERROR,
 | |
| } from '~/design_management/utils/error_messages';
 | |
| import {
 | |
|   DESIGN_TRACKING_PAGE_NAME,
 | |
|   DESIGN_SNOWPLOW_EVENT_TYPES,
 | |
|   DESIGN_SERVICE_PING_EVENT_TYPES,
 | |
| } from '~/design_management/utils/tracking';
 | |
| import { createAlert } from '~/alert';
 | |
| import * as cacheUpdate from '~/design_management/utils/cache_update';
 | |
| import { stubComponent } from 'helpers/stub_component';
 | |
| 
 | |
| import mockAllVersions from '../../mock_data/all_versions';
 | |
| import design from '../../mock_data/design';
 | |
| import mockProject from '../../mock_data/project';
 | |
| import mockResponseWithDesigns from '../../mock_data/designs';
 | |
| import mockResponseNoDesigns from '../../mock_data/no_designs';
 | |
| import { mockCreateImageNoteDiffResponse } from '../../mock_data/apollo_mock';
 | |
| 
 | |
| jest.mock('~/alert');
 | |
| jest.mock('~/api.js');
 | |
| 
 | |
| const mockCacheObject = {
 | |
|   readQuery: jest.fn().mockReturnValue(mockProject),
 | |
|   writeQuery: jest.fn(),
 | |
| };
 | |
| const mutate = jest.fn().mockResolvedValue();
 | |
| const mockPageLayoutElement = {
 | |
|   classList: {
 | |
|     add: jest.fn(),
 | |
|     remove: jest.fn(),
 | |
|   },
 | |
| };
 | |
| const DesignReplyForm = {
 | |
|   template: '<div><textarea ref="textarea"></textarea></div>',
 | |
| };
 | |
| const mockDesignNoDiscussions = {
 | |
|   ...design,
 | |
|   discussions: {
 | |
|     nodes: [],
 | |
|   },
 | |
| };
 | |
| 
 | |
| const annotationCoordinates = {
 | |
|   x: 10,
 | |
|   y: 10,
 | |
|   width: 100,
 | |
|   height: 100,
 | |
| };
 | |
| 
 | |
| Vue.use(VueRouter);
 | |
| 
 | |
| describe('Design management design index page', () => {
 | |
|   let wrapper;
 | |
|   let router;
 | |
| 
 | |
|   const findDesignReplyForm = () => wrapper.findComponent(DesignReplyForm);
 | |
|   const findSidebar = () => wrapper.findComponent(DesignSidebar);
 | |
|   const findDesignPresentation = () => wrapper.findComponent(DesignPresentation);
 | |
| 
 | |
|   function createComponent(
 | |
|     { loading = false } = {},
 | |
|     {
 | |
|       data = {},
 | |
|       intialRouteOptions = {},
 | |
|       provide = {},
 | |
|       stubs = { DesignSidebar, DesignReplyForm },
 | |
|     } = {},
 | |
|   ) {
 | |
|     const $apollo = {
 | |
|       queries: {
 | |
|         design: {
 | |
|           loading,
 | |
|         },
 | |
|       },
 | |
|       mutate,
 | |
|       getClient() {
 | |
|         return {
 | |
|           cache: mockCacheObject,
 | |
|         };
 | |
|       },
 | |
|     };
 | |
| 
 | |
|     router = createRouter();
 | |
| 
 | |
|     router.push({ name: DESIGN_ROUTE_NAME, params: { id: design.id }, ...intialRouteOptions });
 | |
| 
 | |
|     wrapper = shallowMount(DesignIndex, {
 | |
|       propsData: { id: '1' },
 | |
|       mocks: { $apollo },
 | |
|       stubs: {
 | |
|         ...stubs,
 | |
|         DesignDestroyer: stubComponent(DesignDestroyer, { template: '<div></div>' }),
 | |
|       },
 | |
|       provide: {
 | |
|         issueIid: '1',
 | |
|         projectPath: 'project-path',
 | |
|         ...provide,
 | |
|       },
 | |
|       data() {
 | |
|         return {
 | |
|           activeDiscussion: {
 | |
|             id: null,
 | |
|             source: null,
 | |
|           },
 | |
|           ...data,
 | |
|         };
 | |
|       },
 | |
|       router,
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   describe('when navigating to component', () => {
 | |
|     it('applies fullscreen layout class', () => {
 | |
|       jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageLayoutElement);
 | |
|       createComponent({}, { stubs: {} });
 | |
| 
 | |
|       expect(mockPageLayoutElement.classList.add).toHaveBeenCalledTimes(1);
 | |
|       expect(mockPageLayoutElement.classList.add).toHaveBeenCalledWith(
 | |
|         ...DESIGN_DETAIL_LAYOUT_CLASSLIST,
 | |
|       );
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('when navigating within the component', () => {
 | |
|     it('`scale` prop of DesignPresentation component is 1', async () => {
 | |
|       jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageLayoutElement);
 | |
|       createComponent({}, { data: { design, scale: 2 } });
 | |
| 
 | |
|       await nextTick();
 | |
|       expect(findDesignPresentation().props('scale')).toBe(2);
 | |
| 
 | |
|       DesignIndex.beforeRouteUpdate.call(wrapper.vm, {}, {}, jest.fn());
 | |
|       await nextTick();
 | |
| 
 | |
|       expect(findDesignPresentation().props('scale')).toBe(1);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('when navigating away from component', () => {
 | |
|     it('removes fullscreen layout class', () => {
 | |
|       jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageLayoutElement);
 | |
|       createComponent({ loading: true });
 | |
| 
 | |
|       wrapper.vm.$options.beforeRouteLeave[0].call(wrapper.vm, {}, {}, jest.fn());
 | |
| 
 | |
|       expect(mockPageLayoutElement.classList.remove).toHaveBeenCalledTimes(1);
 | |
|       expect(mockPageLayoutElement.classList.remove).toHaveBeenCalledWith(
 | |
|         ...DESIGN_DETAIL_LAYOUT_CLASSLIST,
 | |
|       );
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   it('sets loading state', () => {
 | |
|     createComponent({ loading: true });
 | |
| 
 | |
|     expect(wrapper.findComponent(DesignPresentation).props('isLoading')).toBe(true);
 | |
|     expect(wrapper.findComponent(DesignSidebar).props('isLoading')).toBe(true);
 | |
|   });
 | |
| 
 | |
|   it('renders design index', () => {
 | |
|     createComponent({ loading: false }, { data: { design } });
 | |
| 
 | |
|     expect(wrapper.element).toMatchSnapshot();
 | |
|     expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
 | |
|   });
 | |
| 
 | |
|   it('passes correct props to sidebar component', () => {
 | |
|     createComponent({ loading: false }, { data: { design } });
 | |
| 
 | |
|     expect(findSidebar().props()).toEqual({
 | |
|       design,
 | |
|       markdownPreviewPath: '/project-path/preview_markdown?target_type=Issue',
 | |
|       resolvedDiscussionsExpanded: false,
 | |
|       isLoading: false,
 | |
|       designVariables: {
 | |
|         fullPath: 'project-path',
 | |
|         iid: '1',
 | |
|         filenames: ['gid::/gitlab/Design/1'],
 | |
|         atVersion: null,
 | |
|       },
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   it('opens a new discussion form', async () => {
 | |
|     createComponent(
 | |
|       { loading: false },
 | |
|       {
 | |
|         data: {
 | |
|           design,
 | |
|         },
 | |
|       },
 | |
|     );
 | |
| 
 | |
|     findDesignPresentation().vm.$emit('openCommentForm', { x: 0, y: 0 });
 | |
| 
 | |
|     await nextTick();
 | |
|     expect(findDesignReplyForm().exists()).toBe(true);
 | |
|   });
 | |
| 
 | |
|   it('sends a update and closes the form when mutation is completed', async () => {
 | |
|     createComponent(
 | |
|       { loading: false },
 | |
|       {
 | |
|         data: {
 | |
|           design,
 | |
|           annotationCoordinates,
 | |
|         },
 | |
|       },
 | |
|     );
 | |
| 
 | |
|     const addImageDiffNoteToStore = jest.spyOn(cacheUpdate, 'updateStoreAfterAddImageDiffNote');
 | |
| 
 | |
|     const mockDesignVariables = {
 | |
|       fullPath: 'project-path',
 | |
|       iid: '1',
 | |
|       filenames: ['gid::/gitlab/Design/1'],
 | |
|       atVersion: null,
 | |
|     };
 | |
| 
 | |
|     findDesignReplyForm().vm.$emit('note-submit-complete', mockCreateImageNoteDiffResponse);
 | |
| 
 | |
|     await nextTick();
 | |
|     expect(addImageDiffNoteToStore).toHaveBeenCalledWith(
 | |
|       mockCacheObject,
 | |
|       mockCreateImageNoteDiffResponse.data.createImageDiffNote,
 | |
|       getDesignQuery,
 | |
|       mockDesignVariables,
 | |
|     );
 | |
|     expect(findDesignReplyForm().exists()).toBe(false);
 | |
|   });
 | |
| 
 | |
|   it('closes the form and clears the comment on canceling form', async () => {
 | |
|     createComponent(
 | |
|       { loading: false },
 | |
|       {
 | |
|         data: {
 | |
|           design,
 | |
|           annotationCoordinates,
 | |
|         },
 | |
|       },
 | |
|     );
 | |
| 
 | |
|     findDesignReplyForm().vm.$emit('cancel-form');
 | |
| 
 | |
|     await nextTick();
 | |
|     expect(findDesignReplyForm().exists()).toBe(false);
 | |
|   });
 | |
| 
 | |
|   describe('with error', () => {
 | |
|     beforeEach(() => {
 | |
|       createComponent(
 | |
|         { loading: false },
 | |
|         {
 | |
|           data: {
 | |
|             design: mockDesignNoDiscussions,
 | |
|             errorMessage: 'woops',
 | |
|           },
 | |
|         },
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     it('GlAlert is rendered in correct position with correct content', () => {
 | |
|       expect(wrapper.element).toMatchSnapshot();
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('onDesignQueryResult', () => {
 | |
|     describe('with no designs', () => {
 | |
|       it('redirects to /designs', async () => {
 | |
|         createComponent({ loading: true });
 | |
|         router.push = jest.fn();
 | |
| 
 | |
|         wrapper.vm.onDesignQueryResult({ data: mockResponseNoDesigns, loading: false });
 | |
|         await nextTick();
 | |
|         expect(createAlert).toHaveBeenCalledTimes(1);
 | |
|         expect(createAlert).toHaveBeenCalledWith({ message: DESIGN_NOT_FOUND_ERROR });
 | |
|         expect(router.push).toHaveBeenCalledTimes(1);
 | |
|         expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME });
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('when no design exists for given version', () => {
 | |
|       it('redirects to /designs', async () => {
 | |
|         createComponent({ loading: true });
 | |
|         // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
 | |
|         // eslint-disable-next-line no-restricted-syntax
 | |
|         wrapper.setData({
 | |
|           allVersions: mockAllVersions,
 | |
|         });
 | |
| 
 | |
|         // attempt to query for a version of the design that doesn't exist
 | |
|         router.push({ query: { version: '999' } });
 | |
|         router.push = jest.fn();
 | |
| 
 | |
|         wrapper.vm.onDesignQueryResult({ data: mockResponseWithDesigns, loading: false });
 | |
|         await nextTick();
 | |
|         expect(createAlert).toHaveBeenCalledTimes(1);
 | |
|         expect(createAlert).toHaveBeenCalledWith({ message: DESIGN_VERSION_NOT_EXIST_ERROR });
 | |
|         expect(router.push).toHaveBeenCalledTimes(1);
 | |
|         expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME });
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('when hash present in current route', () => {
 | |
|     it('calls updateActiveDiscussion mutation', () => {
 | |
|       createComponent(
 | |
|         { loading: false },
 | |
|         {
 | |
|           data: {
 | |
|             design,
 | |
|           },
 | |
|           intialRouteOptions: { hash: '#note_123' },
 | |
|         },
 | |
|       );
 | |
| 
 | |
|       expect(mutate).toHaveBeenCalledTimes(1);
 | |
|       expect(mutate).toHaveBeenCalledWith({
 | |
|         mutation: updateActiveDiscussion,
 | |
|         variables: { id: 'gid://gitlab/DiffNote/123', source: 'url' },
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('tracking', () => {
 | |
|     let trackingSpy;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
 | |
|     });
 | |
| 
 | |
|     afterEach(() => {
 | |
|       unmockTracking();
 | |
|     });
 | |
| 
 | |
|     describe('on mount', () => {
 | |
|       it('tracks design view in snowplow', () => {
 | |
|         createComponent({ loading: true });
 | |
| 
 | |
|         expect(trackingSpy).toHaveBeenCalledTimes(1);
 | |
|         expect(trackingSpy).toHaveBeenCalledWith(
 | |
|           DESIGN_TRACKING_PAGE_NAME,
 | |
|           DESIGN_SNOWPLOW_EVENT_TYPES.VIEW_DESIGN,
 | |
|           {
 | |
|             context: {
 | |
|               data: {
 | |
|                 'design-collection-owner': 'issue',
 | |
|                 'design-is-current-version': true,
 | |
|                 'design-version-number': 1,
 | |
|                 'internal-object-referrer': 'issue-design-collection',
 | |
|               },
 | |
|               schema: 'iglu:com.gitlab/design_management_context/jsonschema/1-0-0',
 | |
|             },
 | |
|             label: DESIGN_SNOWPLOW_EVENT_TYPES.VIEW_DESIGN,
 | |
|           },
 | |
|         );
 | |
|       });
 | |
| 
 | |
|       it('tracks design view service ping', () => {
 | |
|         createComponent({ loading: true });
 | |
| 
 | |
|         expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
 | |
|         expect(Api.trackRedisHllUserEvent).toHaveBeenCalledWith(
 | |
|           DESIGN_SERVICE_PING_EVENT_TYPES.DESIGN_ACTION,
 | |
|         );
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| });
 |