import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import DesignOverlay from '~/design_management/components/design_overlay.vue'; import DesignPresentation from '~/design_management/components/design_presentation.vue'; const mockOverlayData = { overlayDimensions: { width: 100, height: 100, }, overlayPosition: { top: '0', left: '0', }, }; describe('Design management design presentation component', () => { const originalGon = window.gon; let wrapper; function createComponent( { image, imageName, discussions = [], isAnnotating = false, resolvedDiscussionsExpanded = false, } = {}, data = {}, stubs = {}, ) { wrapper = shallowMount(DesignPresentation, { propsData: { image, imageName, discussions, isAnnotating, resolvedDiscussionsExpanded, isLoading: false, }, stubs, }); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData(data); wrapper.element.scrollTo = jest.fn(); } const findOverlayCommentButton = () => wrapper.find('[data-qa-selector="design_image_button"]'); /** * Spy on $refs and mock given values * @param {Object} viewportDimensions {width, height} * @param {Object} childDimensions {width, height} * @param {Float} scrollTopPerc 0 < x < 1 * @param {Float} scrollLeftPerc 0 < x < 1 */ function mockRefDimensions( ref, viewportDimensions, childDimensions, scrollTopPerc, scrollLeftPerc, ) { jest.spyOn(ref, 'scrollWidth', 'get').mockReturnValue(childDimensions.width); jest.spyOn(ref, 'scrollHeight', 'get').mockReturnValue(childDimensions.height); jest.spyOn(ref, 'offsetWidth', 'get').mockReturnValue(viewportDimensions.width); jest.spyOn(ref, 'offsetHeight', 'get').mockReturnValue(viewportDimensions.height); jest .spyOn(ref, 'scrollLeft', 'get') .mockReturnValue((childDimensions.width - viewportDimensions.width) * scrollLeftPerc); jest .spyOn(ref, 'scrollTop', 'get') .mockReturnValue((childDimensions.height - viewportDimensions.height) * scrollTopPerc); } async function clickDragExplore(startCoords, endCoords, { useTouchEvents, mouseup } = {}) { const event = useTouchEvents ? { mousedown: 'touchstart', mousemove: 'touchmove', mouseup: 'touchend', } : { mousedown: 'mousedown', mousemove: 'mousemove', mouseup: 'mouseup', }; const addCommentOverlay = findOverlayCommentButton(); // triggering mouse events on this element best simulates // reality, as it is the lowest-level node that needs to // respond to mouse events addCommentOverlay.trigger(event.mousedown, { clientX: startCoords.clientX, clientY: startCoords.clientY, }); await nextTick(); addCommentOverlay.trigger(event.mousemove, { clientX: endCoords.clientX, clientY: endCoords.clientY, }); await nextTick(); if (mouseup) { addCommentOverlay.trigger(event.mouseup); await nextTick(); } } beforeEach(() => { window.gon = { current_user_id: 1 }; }); afterEach(() => { wrapper.destroy(); window.gon = originalGon; }); it('renders image and overlay when image provided', async () => { createComponent( { image: 'test.jpg', imageName: 'test', }, mockOverlayData, ); await nextTick(); expect(wrapper.element).toMatchSnapshot(); }); it('renders empty state when no image provided', async () => { createComponent(); await nextTick(); expect(wrapper.element).toMatchSnapshot(); }); it('openCommentForm event emits correct data', async () => { createComponent( { image: 'test.jpg', imageName: 'test', }, mockOverlayData, ); wrapper.vm.openCommentForm({ x: 1, y: 1 }); await nextTick(); expect(wrapper.emitted('openCommentForm')).toEqual([ [{ ...mockOverlayData.overlayDimensions, x: 1, y: 1 }], ]); }); describe('currentCommentForm', () => { it('is null when isAnnotating is false', async () => { createComponent( { image: 'test.jpg', imageName: 'test', }, mockOverlayData, ); await nextTick(); expect(wrapper.vm.currentCommentForm).toBeNull(); expect(wrapper.element).toMatchSnapshot(); }); it('is null when isAnnotating is true but annotation position is falsey', async () => { createComponent( { image: 'test.jpg', imageName: 'test', isAnnotating: true, }, mockOverlayData, ); await nextTick(); expect(wrapper.vm.currentCommentForm).toBeNull(); expect(wrapper.element).toMatchSnapshot(); }); it('is equal to current annotation position when isAnnotating is true', async () => { createComponent( { image: 'test.jpg', imageName: 'test', isAnnotating: true, }, { ...mockOverlayData, currentAnnotationPosition: { x: 1, y: 1, width: 100, height: 100, }, }, ); await nextTick(); expect(wrapper.vm.currentCommentForm).toEqual({ x: 1, y: 1, width: 100, height: 100, }); expect(wrapper.element).toMatchSnapshot(); }); }); describe('setOverlayPosition', () => { beforeEach(() => { createComponent( { image: 'test.jpg', imageName: 'test', }, mockOverlayData, ); }); afterEach(() => { jest.clearAllMocks(); }); it('sets overlay position correctly when overlay is smaller than viewport', () => { jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(200); jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(200); wrapper.vm.setOverlayPosition(); expect(wrapper.vm.overlayPosition).toEqual({ left: `calc(50% - ${mockOverlayData.overlayDimensions.width / 2}px)`, top: `calc(50% - ${mockOverlayData.overlayDimensions.height / 2}px)`, }); }); it('sets overlay position correctly when overlay width is larger than viewports', () => { jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(50); jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(200); wrapper.vm.setOverlayPosition(); expect(wrapper.vm.overlayPosition).toEqual({ left: '0', top: `calc(50% - ${mockOverlayData.overlayDimensions.height / 2}px)`, }); }); it('sets overlay position correctly when overlay height is larger than viewports', () => { jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(200); jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(50); wrapper.vm.setOverlayPosition(); expect(wrapper.vm.overlayPosition).toEqual({ left: `calc(50% - ${mockOverlayData.overlayDimensions.width / 2}px)`, top: '0', }); }); }); describe('getViewportCenter', () => { beforeEach(() => { createComponent( { image: 'test.jpg', imageName: 'test', }, mockOverlayData, ); }); it('calculate center correctly with no scroll', () => { mockRefDimensions( wrapper.vm.$refs.presentationViewport, { width: 10, height: 10 }, { width: 20, height: 20 }, 0, 0, ); expect(wrapper.vm.getViewportCenter()).toEqual({ x: 5, y: 5, }); }); it('calculate center correctly with some scroll', () => { mockRefDimensions( wrapper.vm.$refs.presentationViewport, { width: 10, height: 10 }, { width: 20, height: 20 }, 0.5, 0.5, ); expect(wrapper.vm.getViewportCenter()).toEqual({ x: 10, y: 10, }); }); it('Returns default case if no overflow (scrollWidth==offsetWidth, etc.)', () => { mockRefDimensions( wrapper.vm.$refs.presentationViewport, { width: 20, height: 20 }, { width: 20, height: 20 }, 0.5, 0.5, ); expect(wrapper.vm.getViewportCenter()).toEqual({ x: 10, y: 10, }); }); }); describe('scaleZoomFocalPoint', () => { it('scales focal point correctly when zooming in', () => { createComponent( { image: 'test.jpg', imageName: 'test', }, { ...mockOverlayData, zoomFocalPoint: { x: 5, y: 5, width: 50, height: 50, }, }, ); wrapper.vm.scaleZoomFocalPoint(); expect(wrapper.vm.zoomFocalPoint).toEqual({ x: 10, y: 10, width: 100, height: 100, }); }); it('scales focal point correctly when zooming out', () => { createComponent( { image: 'test.jpg', imageName: 'test', }, { ...mockOverlayData, zoomFocalPoint: { x: 10, y: 10, width: 200, height: 200, }, }, ); wrapper.vm.scaleZoomFocalPoint(); expect(wrapper.vm.zoomFocalPoint).toEqual({ x: 5, y: 5, width: 100, height: 100, }); }); }); describe('onImageResize', () => { beforeEach(async () => { createComponent( { image: 'test.jpg', imageName: 'test', }, mockOverlayData, ); jest.spyOn(wrapper.vm, 'shiftZoomFocalPoint'); jest.spyOn(wrapper.vm, 'scaleZoomFocalPoint'); jest.spyOn(wrapper.vm, 'scrollToFocalPoint'); wrapper.vm.onImageResize({ width: 10, height: 10 }); await nextTick(); }); it('sets zoom focal point on initial load', () => { expect(wrapper.vm.shiftZoomFocalPoint).toHaveBeenCalled(); expect(wrapper.vm.initialLoad).toBe(false); }); it('calls scaleZoomFocalPoint and scrollToFocalPoint after initial load', async () => { wrapper.vm.onImageResize({ width: 10, height: 10 }); await nextTick(); expect(wrapper.vm.scaleZoomFocalPoint).toHaveBeenCalled(); expect(wrapper.vm.scrollToFocalPoint).toHaveBeenCalled(); }); }); describe('onPresentationMousedown', () => { it.each` scenario | width | height ${'width overflows'} | ${101} | ${100} ${'height overflows'} | ${100} | ${101} ${'width and height overflows'} | ${200} | ${200} `('sets lastDragPosition when design $scenario', ({ width, height }) => { createComponent(); mockRefDimensions( wrapper.vm.$refs.presentationViewport, { width: 100, height: 100 }, { width, height }, ); const newLastDragPosition = { x: 2, y: 2 }; wrapper.vm.onPresentationMousedown({ clientX: newLastDragPosition.x, clientY: newLastDragPosition.y, }); expect(wrapper.vm.lastDragPosition).toStrictEqual(newLastDragPosition); }); it('does not set lastDragPosition if design does not overflow', () => { const lastDragPosition = { x: 1, y: 1 }; createComponent({}, { lastDragPosition }); mockRefDimensions( wrapper.vm.$refs.presentationViewport, { width: 100, height: 100 }, { width: 50, height: 50 }, ); wrapper.vm.onPresentationMousedown({ clientX: 2, clientY: 2 }); // check lastDragPosition is unchanged expect(wrapper.vm.lastDragPosition).toStrictEqual(lastDragPosition); }); }); describe('getAnnotationPositon', () => { it.each` coordinates | overlayDimensions | position ${{ x: 100, y: 100 }} | ${{ width: 50, height: 50 }} | ${{ x: 100, y: 100, width: 50, height: 50 }} ${{ x: 100.2, y: 100.5 }} | ${{ width: 50.6, height: 50.0 }} | ${{ x: 100, y: 101, width: 51, height: 50 }} `('returns correct annotation position', ({ coordinates, overlayDimensions, position }) => { createComponent(undefined, { overlayDimensions: { width: overlayDimensions.width, height: overlayDimensions.height, }, }); expect(wrapper.vm.getAnnotationPositon(coordinates)).toStrictEqual(position); }); }); describe('when design is overflowing', () => { beforeEach(() => { createComponent( { image: 'test.jpg', imageName: 'test', }, mockOverlayData, { 'design-overlay': DesignOverlay, }, ); // mock a design that overflows mockRefDimensions( wrapper.vm.$refs.presentationViewport, { width: 10, height: 10 }, { width: 20, height: 20 }, 0, 0, ); }); it('opens a comment form if design was not dragged', async () => { const addCommentOverlay = findOverlayCommentButton(); const startCoords = { clientX: 1, clientY: 1, }; addCommentOverlay.trigger('mousedown', { clientX: startCoords.clientX, clientY: startCoords.clientY, }); await nextTick(); addCommentOverlay.trigger('mouseup'); await nextTick(); expect(wrapper.emitted('openCommentForm')).toBeDefined(); }); describe('when clicking and dragging', () => { it.each` description | useTouchEvents ${'with touch events'} | ${true} ${'without touch events'} | ${false} `('calls scrollTo with correct arguments $description', ({ useTouchEvents }) => { return clickDragExplore( { clientX: 0, clientY: 0 }, { clientX: 10, clientY: 10 }, { useTouchEvents }, ).then(() => { expect(wrapper.element.scrollTo).toHaveBeenCalledTimes(1); expect(wrapper.element.scrollTo).toHaveBeenCalledWith(-10, -10); }); }); it('does not open a comment form when drag position exceeds buffer', () => { return clickDragExplore( { clientX: 0, clientY: 0 }, { clientX: 10, clientY: 10 }, { mouseup: true }, ).then(() => { expect(wrapper.emitted('openCommentForm')).toBeFalsy(); }); }); it('opens a comment form when drag position is within buffer', () => { return clickDragExplore( { clientX: 0, clientY: 0 }, { clientX: 1, clientY: 0 }, { mouseup: true }, ).then(() => { expect(wrapper.emitted('openCommentForm')).toBeDefined(); }); }); }); }); describe('when user is not logged in', () => { beforeEach(() => { window.gon = { current_user_id: null }; createComponent( { image: 'test.jpg', imageName: 'test', }, mockOverlayData, ); }); it('disables commenting from design overlay', () => { expect(wrapper.findComponent(DesignOverlay).props()).toMatchObject({ disableCommenting: true, }); }); }); });