Commit 9fee239f authored by Tom Quirk's avatar Tom Quirk Committed by Natalia Tepluhina

Add design_scaler component

This component is intended to be the
UI entry point for scaling/zooming a design.

It emits a @scale event, and holds the increment
and constraint logic for updating scale
parent c60e8aa1
<script>
import _ from 'underscore';
import DesignImage from './image.vue';
import DesignOverlay from './design_overlay.vue';
......@@ -27,12 +28,24 @@ export default {
required: false,
default: false,
},
scale: {
type: Number,
required: false,
default: 1,
},
},
data() {
return {
overlayDimensions: null,
overlayPosition: null,
currentAnnotationCoordinates: null,
zoomFocalPoint: {
x: 0,
y: 0,
width: 0,
height: 0,
},
initialLoad: true,
};
},
computed: {
......@@ -43,6 +56,22 @@ export default {
return (this.isAnnotating && this.currentAnnotationCoordinates) || null;
},
},
beforeDestroy() {
const { presentationViewport } = this.$refs;
if (!presentationViewport) return;
presentationViewport.removeEventListener('scroll', this.scrollThrottled, false);
},
mounted() {
const { presentationViewport } = this.$refs;
if (!presentationViewport) return;
this.scrollThrottled = _.throttle(() => {
this.shiftZoomFocalPoint();
}, 400);
presentationViewport.addEventListener('scroll', this.scrollThrottled, false);
},
methods: {
setOverlayDimensions(overlayDimensions) {
this.overlayDimensions = overlayDimensions;
......@@ -52,8 +81,8 @@ export default {
this.overlayPosition = {};
}
const { presentationViewport } = this.$refs;
if (!presentationViewport) return;
const { presentationContainer } = this.$refs;
if (!presentationContainer) return;
// default to center
this.overlayPosition = {
......@@ -62,16 +91,88 @@ export default {
};
// if the overlay overflows, then don't center
if (this.overlayDimensions.width > presentationViewport.offsetWidth) {
if (this.overlayDimensions.width > presentationContainer.offsetWidth) {
this.overlayPosition.left = '0';
}
if (this.overlayDimensions.height > presentationViewport.offsetHeight) {
if (this.overlayDimensions.height > presentationContainer.offsetHeight) {
this.overlayPosition.top = '0';
}
},
/**
* Return a point that represents the center of an
* overflowing child element w.r.t it's parent
*/
getViewportCenter() {
const { presentationViewport } = this.$refs;
if (!presentationViewport) return {};
// get height of scroll bars (i.e. the max values for scrollTop, scrollLeft)
const scrollBarWidth = presentationViewport.scrollWidth - presentationViewport.offsetWidth;
const scrollBarHeight = presentationViewport.scrollHeight - presentationViewport.offsetHeight;
// determine how many child pixels have been scrolled
const xScrollRatio =
presentationViewport.scrollLeft > 0 ? presentationViewport.scrollLeft / scrollBarWidth : 0;
const yScrollRatio =
presentationViewport.scrollTop > 0 ? presentationViewport.scrollTop / scrollBarHeight : 0;
const xScrollOffset =
(presentationViewport.scrollWidth - presentationViewport.offsetWidth - 0) * xScrollRatio;
const yScrollOffset =
(presentationViewport.scrollHeight - presentationViewport.offsetHeight - 0) * yScrollRatio;
const viewportCenterX = presentationViewport.offsetWidth / 2;
const viewportCenterY = presentationViewport.offsetHeight / 2;
const focalPointX = viewportCenterX + xScrollOffset;
const focalPointY = viewportCenterY + yScrollOffset;
return {
x: focalPointX,
y: focalPointY,
};
},
/**
* Scroll the viewport such that the focal point is positioned centrally
*/
scrollToFocalPoint() {
const { presentationViewport } = this.$refs;
if (!presentationViewport) return;
const scrollX = this.zoomFocalPoint.x - presentationViewport.offsetWidth / 2;
const scrollY = this.zoomFocalPoint.y - presentationViewport.offsetHeight / 2;
presentationViewport.scrollTo(scrollX, scrollY);
},
scaleZoomFocalPoint() {
const { x, y, width, height } = this.zoomFocalPoint;
const widthRatio = this.overlayDimensions.width / width;
const heightRatio = this.overlayDimensions.height / height;
this.zoomFocalPoint = {
x: Math.round(x * widthRatio * 100) / 100,
y: Math.round(y * heightRatio * 100) / 100,
...this.overlayDimensions,
};
},
shiftZoomFocalPoint() {
this.zoomFocalPoint = {
...this.getViewportCenter(),
...this.overlayDimensions,
};
},
onImageResize(imageDimensions) {
this.setOverlayDimensions(imageDimensions);
this.setOverlayPosition();
this.$nextTick(() => {
if (this.initialLoad) {
// set focal point on initial load
this.shiftZoomFocalPoint();
this.initialLoad = false;
} else {
this.scaleZoomFocalPoint();
this.scrollToFocalPoint();
}
});
},
openCommentForm(position) {
const { x, y } = position;
......@@ -89,9 +190,18 @@ export default {
</script>
<template>
<div ref="presentationViewport" class="h-100 w-100 p-3 overflow-auto">
<div class="h-100 w-100 d-flex align-items-center position-relative">
<design-image v-if="image" :image="image" :name="imageName" @resize="onImageResize" />
<div ref="presentationViewport" class="h-100 w-100 p-3 overflow-auto position-relative">
<div
ref="presentationContainer"
class="h-100 w-100 d-flex align-items-center position-relative"
>
<design-image
v-if="image"
:image="image"
:name="imageName"
:scale="scale"
@resize="onImageResize"
/>
<design-overlay
v-if="overlayDimensions && overlayPosition"
:dimensions="overlayDimensions"
......
<script>
import { GlIcon } from '@gitlab/ui';
const SCALE_STEP_SIZE = 0.2;
const DEFAULT_SCALE = 1;
const MIN_SCALE = 1;
const MAX_SCALE = 2;
export default {
components: {
GlIcon,
},
data() {
return {
scale: DEFAULT_SCALE,
};
},
computed: {
disableReset() {
return this.scale <= MIN_SCALE;
},
disableDecrease() {
return this.scale === DEFAULT_SCALE;
},
disableIncrease() {
return this.scale >= MAX_SCALE;
},
},
methods: {
setScale(scale) {
if (scale < MIN_SCALE) {
return;
}
this.scale = Math.round(scale * 100) / 100;
this.$emit('scale', this.scale);
},
incrementScale() {
this.setScale(this.scale + SCALE_STEP_SIZE);
},
decrementScale() {
this.setScale(this.scale - SCALE_STEP_SIZE);
},
resetScale() {
this.setScale(DEFAULT_SCALE);
},
},
};
</script>
<template>
<div class="design-scaler btn-group" role="group">
<button class="btn" :disabled="disableDecrease" @click="decrementScale">
<span class="d-flex-center gl-icon s16">
</span>
</button>
<button class="btn" :disabled="disableReset" @click="resetScale">
<gl-icon name="redo" />
</button>
<button class="btn" :disabled="disableIncrease" @click="incrementScale">
<gl-icon name="plus" />
</button>
</div>
</template>
......@@ -13,46 +13,85 @@ export default {
required: false,
default: '',
},
scale: {
type: Number,
required: false,
default: 1,
},
},
data() {
return {
baseImageSize: null,
imageStyle: null,
};
},
watch: {
scale(val) {
this.zoom(val);
},
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeThrottled, false);
},
mounted() {
this.onImgLoad();
this.resizeThrottled = _.throttle(this.onImgLoad, 400);
this.resizeThrottled = _.throttle(() => {
this.onWindowResize();
}, 400);
window.addEventListener('resize', this.resizeThrottled, false);
},
methods: {
onImgLoad() {
requestIdleCallback(this.calculateImgSize, { timeout: 1000 });
requestIdleCallback(this.setBaseImageSize, { timeout: 1000 });
},
calculateImgSize() {
onWindowResize() {
const { contentImg } = this.$refs;
if (!contentImg) return;
this.$nextTick(() => {
const naturalRatio = contentImg.naturalWidth / contentImg.naturalHeight;
const visibleRatio = contentImg.width / contentImg.height;
this.onResize({
width: contentImg.offsetWidth,
height: contentImg.offsetHeight,
});
},
setBaseImageSize() {
const { contentImg } = this.$refs;
if (!contentImg || contentImg.offsetHeight === 0 || contentImg.offsetWidth === 0) return;
const position = {
// Handling the case where img element takes more width than visible image thanks to object-fit: contain
width:
naturalRatio < visibleRatio
? contentImg.clientHeight * naturalRatio
: contentImg.clientWidth,
height: contentImg.clientHeight,
};
this.baseImageSize = {
height: contentImg.offsetHeight,
width: contentImg.offsetWidth,
};
this.onResize({ width: this.baseImageSize.width, height: this.baseImageSize.height });
},
onResize({ width, height }) {
this.$emit('resize', { width, height });
},
zoom(amount) {
const width = this.baseImageSize.width * amount;
const height = this.baseImageSize.height * amount;
this.$emit('resize', position);
});
this.imageStyle = {
width: `${width}px`,
height: `${height}px`,
};
this.onResize({ width, height });
},
},
};
</script>
<template>
<div class="m-auto h-100 w-100 d-flex-center js-design-image">
<img ref="contentImg" :src="image" :alt="name" class="img-fluid mh-100" @load="onImgLoad" />
<div class="m-auto js-design-image" :class="{ 'h-100 w-100 d-flex-center': !imageStyle }">
<img
ref="contentImg"
class="mh-100"
:src="image"
:alt="name"
:style="imageStyle"
:class="{ 'img-fluid': !imageStyle }"
@load="onImgLoad"
/>
</div>
</template>
......@@ -8,6 +8,7 @@ import Toolbar from '../../components/toolbar/index.vue';
import DesignDiscussion from '../../components/design_notes/design_discussion.vue';
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
import DesignDestroyer from '../../components/design_destroyer.vue';
import DesignScaler from '../../components/design_scaler.vue';
import Participants from '~/sidebar/components/participants/participants.vue';
import DesignPresentation from '../../components/design_presentation.vue';
import getDesignQuery from '../../graphql/queries/getDesign.query.graphql';
......@@ -31,6 +32,7 @@ export default {
ApolloMutation,
DesignPresentation,
DesignDiscussion,
DesignScaler,
DesignDestroyer,
Toolbar,
DesignReplyForm,
......@@ -53,6 +55,7 @@ export default {
projectPath: '',
errorMessage: '',
issueIid: '',
scale: 1,
};
},
apollo: {
......@@ -202,7 +205,7 @@ export default {
>
<gl-loading-icon v-if="isLoading" size="xl" class="align-self-center" />
<template v-else>
<div class="d-flex overflow-hidden flex-lg-grow-1 flex-column">
<div class="d-flex overflow-hidden flex-grow-1 flex-column position-relative">
<design-destroyer
:filenames="[design.filename]"
:project-path="projectPath"
......@@ -226,14 +229,17 @@ export default {
{{ errorMessage }}
</gl-alert>
</div>
<design-presentation
:image="design.image"
:image-name="design.filename"
:discussions="discussions"
:is-annotating="isAnnotating"
:scale="scale"
@openCommentForm="openCommentForm"
/>
<div class="design-scaler-wrapper position-absolute w-100 pb-4 d-flex-center">
<design-scaler @scale="scale = $event" />
</div>
</div>
<div class="image-notes">
<h2 class="gl-font-size-20 font-weight-bold mt-0">{{ issue.title }}</h2>
......
.design-image {
object-fit: contain;
}
.design-presentation-wrapper {
top: 0;
left: 0;
}
.design-scaler {
z-index: 1;
}
.design-scaler-wrapper {
bottom: 0;
left: 0;
}
.design-list-item .design-event {
top: $gl-padding;
right: $gl-padding;
......
---
title: Add zooming functionality to designs in Design view
merge_request: 22863
author:
type: added
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management design presentation component currentCommentForm currentCommentForm is equal to current annotation coordinates when isAnnotating is true 1`] = `
<div
class="h-100 w-100 p-3 overflow-auto position-relative"
>
<div
class="h-100 w-100 d-flex align-items-center position-relative"
>
<design-image-stub
image="test.jpg"
name="test"
scale="1"
/>
<design-overlay-stub
currentcommentform="[object Object]"
dimensions="[object Object]"
notes=""
position="[object Object]"
/>
</div>
</div>
`;
exports[`Design management design presentation component currentCommentForm currentCommentForm is null when isAnnotating is false 1`] = `
<div
class="h-100 w-100 p-3 overflow-auto position-relative"
>
<div
class="h-100 w-100 d-flex align-items-center position-relative"
>
<design-image-stub
image="test.jpg"
name="test"
scale="1"
/>
<design-overlay-stub
dimensions="[object Object]"
notes=""
position="[object Object]"
/>
</div>
</div>
`;
exports[`Design management design presentation component currentCommentForm currentCommentForm is null when isAnnotating is true but annotation coordinates are falsey 1`] = `
<div
class="h-100 w-100 p-3 overflow-auto position-relative"
>
<div
class="h-100 w-100 d-flex align-items-center position-relative"
>
<design-image-stub
image="test.jpg"
name="test"
scale="1"
/>
<design-overlay-stub
dimensions="[object Object]"
notes=""
position="[object Object]"
/>
</div>
</div>
`;
exports[`Design management design presentation component renders empty state when no image provided 1`] = `
<div
class="h-100 w-100 p-3 overflow-auto"
class="h-100 w-100 p-3 overflow-auto position-relative"
>
<div
class="h-100 w-100 d-flex align-items-center position-relative"
......@@ -16,7 +83,7 @@ exports[`Design management design presentation component renders empty state whe
exports[`Design management design presentation component renders image and overlay when image provided 1`] = `
<div
class="h-100 w-100 p-3 overflow-auto"
class="h-100 w-100 p-3 overflow-auto position-relative"
>
<div
class="h-100 w-100 d-flex align-items-center position-relative"
......@@ -24,6 +91,7 @@ exports[`Design management design presentation component renders image and overl
<design-image-stub
image="test.jpg"
name="test"
scale="1"
/>
<design-overlay-stub
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management design scaler component minus and reset buttons are disabled when scale === 1 1`] = `
<div
class="design-scaler btn-group"
role="group"
>
<button
class="btn"
disabled="disabled"
>
<span
class="d-flex-center gl-icon s16"
>
</span>
</button>
<button
class="btn"
disabled="disabled"
>
<gl-icon-stub
name="redo"
size="16"
/>
</button>
<button
class="btn"
>
<gl-icon-stub
name="plus"
size="16"
/>
</button>
</div>
`;
exports[`Design management design scaler component minus and reset buttons are enabled when scale > 1 1`] = `
<div
class="design-scaler btn-group"
role="group"
>
<button
class="btn"
>
<span
class="d-flex-center gl-icon s16"
>
</span>
</button>
<button
class="btn"
>
<gl-icon-stub
name="redo"
size="16"
/>
</button>
<button
class="btn"
>
<gl-icon-stub
name="plus"
size="16"
/>
</button>
</div>
`;
exports[`Design management design scaler component plus button is disabled when scale === 2 1`] = `
<div
class="design-scaler btn-group"
role="group"
>
<button
class="btn"
>
<span
class="d-flex-center gl-icon s16"
>
</span>
</button>
<button
class="btn"
>
<gl-icon-stub
name="redo"
size="16"
/>
</button>
<button
class="btn"
disabled="disabled"
>
<gl-icon-stub
name="plus"
size="16"
/>
</button>
</div>
`;
......@@ -2,11 +2,11 @@
exports[`Design management large image component renders image 1`] = `
<div
class="m-auto h-100 w-100 d-flex-center js-design-image"
class="m-auto js-design-image h-100 w-100 d-flex-center"
>
<img
alt="test"
class="img-fluid mh-100"
class="mh-100 img-fluid"
src="test.jpg"
/>
</div>
......@@ -14,13 +14,39 @@ exports[`Design management large image component renders image 1`] = `
exports[`Design management large image component renders loading state 1`] = `
<div
class="m-auto h-100 w-100 d-flex-center js-design-image"
class="m-auto js-design-image h-100 w-100 d-flex-center"
isloading="true"
>
<img
alt=""
class="img-fluid mh-100"
class="mh-100 img-fluid"
src=""
/>
</div>
`;
exports[`Design management large image component sets correct classes and styles if imageStyle is set 1`] = `
<div
class="m-auto js-design-image"
>
<img
alt="test"
class="mh-100"
src="test.jpg"
style="width: 100px; height: 100px;"
/>
</div>
`;
exports[`Design management large image component zoom sets image style when zoomed 1`] = `
<div
class="m-auto js-design-image"
>
<img
alt="test"
class="mh-100"
src="test.jpg"
style="width: 200px; height: 200px;"
/>
</div>
`;
......@@ -75,47 +75,64 @@ describe('Design management design presentation component', () => {
});
});
it('currentCommentForm is null when isAnnotating is false', () => {
createComponent({
image: 'test.jpg',
imageName: 'test',
describe('currentCommentForm', () => {
it('currentCommentForm is null when isAnnotating is false', () => {
createComponent(
{
image: 'test.jpg',
imageName: 'test',
},
mockOverlayData,
);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.currentCommentForm).toBeNull();
expect(wrapper.element).toMatchSnapshot();
});
});
expect(wrapper.vm.currentCommentForm).toBeNull();
});
it('currentCommentForm is null when isAnnotating is true but annotation coordinates are falsey', () => {
createComponent(
{
image: 'test.jpg',
imageName: 'test',
isAnnotating: true,
},
mockOverlayData,
);
it('currentCommentForm is null when isAnnotating is true but annotation coordinates are falsey', () => {
createComponent({
image: 'test.jpg',
imageName: 'test',
isAnnotating: true,
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.currentCommentForm).toBeNull();
expect(wrapper.element).toMatchSnapshot();
});
});
expect(wrapper.vm.currentCommentForm).toBeNull();
});
it('currentCommentForm is equal to current annotation coordinates when isAnnotating is true', () => {
createComponent(
{
image: 'test.jpg',
imageName: 'test',
isAnnotating: true,
},
{
currentAnnotationCoordinates: {
it('currentCommentForm is equal to current annotation coordinates when isAnnotating is true', () => {
createComponent(
{
image: 'test.jpg',
imageName: 'test',
isAnnotating: true,
},
{
...mockOverlayData,
currentAnnotationCoordinates: {
x: 1,
y: 1,
width: 100,
height: 100,
},
},
);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.currentCommentForm).toEqual({
x: 1,
y: 1,
width: 100,
height: 100,
},
},
);
expect(wrapper.vm.currentCommentForm).toEqual({
x: 1,
y: 1,
width: 100,
height: 100,
});
expect(wrapper.element).toMatchSnapshot();
});
});
});
......@@ -135,8 +152,10 @@ describe('Design management design presentation component', () => {
});
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);
jest.spyOn(wrapper.vm.$refs.presentationContainer, 'offsetWidth', 'get').mockReturnValue(200);
jest
.spyOn(wrapper.vm.$refs.presentationContainer, 'offsetHeight', 'get')
.mockReturnValue(200);
wrapper.vm.setOverlayPosition();
expect(wrapper.vm.overlayPosition).toEqual({
......@@ -146,8 +165,10 @@ describe('Design management design presentation component', () => {
});
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);
jest.spyOn(wrapper.vm.$refs.presentationContainer, 'offsetWidth', 'get').mockReturnValue(50);
jest
.spyOn(wrapper.vm.$refs.presentationContainer, 'offsetHeight', 'get')
.mockReturnValue(200);
wrapper.vm.setOverlayPosition();
expect(wrapper.vm.overlayPosition).toEqual({
......@@ -157,8 +178,8 @@ describe('Design management design presentation component', () => {
});
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);
jest.spyOn(wrapper.vm.$refs.presentationContainer, 'offsetWidth', 'get').mockReturnValue(200);
jest.spyOn(wrapper.vm.$refs.presentationContainer, 'offsetHeight', 'get').mockReturnValue(50);
wrapper.vm.setOverlayPosition();
expect(wrapper.vm.overlayPosition).toEqual({
......@@ -167,4 +188,162 @@ describe('Design management design presentation component', () => {
});
});
});
describe('getViewportCenter', () => {
/**
* Spy on $refs.presentationViewport with given values
* @param {Object} viewportDimensions {width, height}
* @param {Object} childDimensions {width, height}
* @param {Float} scrollTopPerc 0 < x < 1
* @param {Float} scrollLeftPerc 0 < x < 1
*/
const spyOnPresentationViewport = (
viewportDimensions,
childDimensions,
scrollTopPerc,
scrollLeftPerc,
) => {
jest
.spyOn(wrapper.vm.$refs.presentationViewport, 'scrollWidth', 'get')
.mockReturnValue(childDimensions.width);
jest
.spyOn(wrapper.vm.$refs.presentationViewport, 'scrollHeight', 'get')
.mockReturnValue(childDimensions.height);
jest
.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get')
.mockReturnValue(viewportDimensions.width);
jest
.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get')
.mockReturnValue(viewportDimensions.height);
jest
.spyOn(wrapper.vm.$refs.presentationViewport, 'scrollLeft', 'get')
.mockReturnValue((childDimensions.width - viewportDimensions.width) * scrollLeftPerc);
jest
.spyOn(wrapper.vm.$refs.presentationViewport, 'scrollTop', 'get')
.mockReturnValue((childDimensions.height - viewportDimensions.height) * scrollTopPerc);
};
beforeEach(() => {
createComponent(
{
image: 'test.jpg',
imageName: 'test',
},
mockOverlayData,
);
});
it('calculate center correctly with no scroll', () => {
spyOnPresentationViewport({ 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', () => {
spyOnPresentationViewport({ 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.)', () => {
spyOnPresentationViewport({ width: 20, height: 20 }, { width: 20, height: 20 }, 0.5, 0.5);
expect(wrapper.vm.getViewportCenter()).toEqual({
x: 10,
y: 10,
});
});
});
describe('scaleZoomFocalPoint', () => {
it('scaleZoomFocalPoint 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('scaleZoomFocalPoint 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', () => {
it('sets zoom focal point on initial load', () => {
createComponent(
{
image: 'test.jpg',
imageName: 'test',
},
mockOverlayData,
);
wrapper.setMethods({
shiftZoomFocalPoint: jest.fn(),
scaleZoomFocalPoint: jest.fn(),
scrollToFocalPoint: jest.fn(),
});
wrapper.vm.onImageResize({ width: 10, height: 10 });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.shiftZoomFocalPoint).toHaveBeenCalled();
expect(wrapper.vm.initialLoad).toBe(false);
});
});
it('calls scaleZoomFocalPoint and scrollToFocalPoint after initial load', () => {
wrapper.vm.onImageResize({ width: 10, height: 10 });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.scaleZoomFocalPoint).toHaveBeenCalled();
expect(wrapper.vm.scrollToFocalPoint).toHaveBeenCalled();
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import DesignScaler from 'ee/design_management/components/design_scaler.vue';
describe('Design management design scaler component', () => {
let wrapper;
function createComponent(propsData, data = {}) {
wrapper = shallowMount(DesignScaler, {
propsData,
});
wrapper.setData(data);
}
afterEach(() => {
wrapper.destroy();
});
const getButton = type => {
const buttonTypeOrder = ['minus', 'reset', 'plus'];
const buttons = wrapper.findAll('button');
return buttons.at(buttonTypeOrder.indexOf(type));
};
it('emits @scale event when "plus" button clicked', () => {
createComponent();
getButton('plus').trigger('click');
expect(wrapper.emitted('scale')).toEqual([[1.2]]);
});
it('emits @scale event when "reset" button clicked (scale > 1)', () => {
createComponent({}, { scale: 1.6 });
return wrapper.vm.$nextTick().then(() => {
getButton('reset').trigger('click');
expect(wrapper.emitted('scale')).toEqual([[1]]);
});
});
it('emits @scale event when "minus" button clicked (scale > 1)', () => {
createComponent({}, { scale: 1.6 });
return wrapper.vm.$nextTick().then(() => {
getButton('minus').trigger('click');
expect(wrapper.emitted('scale')).toEqual([[1.4]]);
});
});
it('minus and reset buttons are disabled when scale === 1', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('minus and reset buttons are enabled when scale > 1', () => {
createComponent({}, { scale: 1.2 });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.element).toMatchSnapshot();
});
});
it('plus button is disabled when scale === 2', () => {
createComponent({}, { scale: 2 });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.element).toMatchSnapshot();
});
});
});
......@@ -4,10 +4,11 @@ import DesignImage from 'ee/design_management/components/image.vue';
describe('Design management large image component', () => {
let wrapper;
function createComponent(propsData) {
function createComponent(propsData, data = {}) {
wrapper = shallowMount(DesignImage, {
propsData,
});
wrapper.setData(data);
}
afterEach(() => {
......@@ -31,4 +32,69 @@ describe('Design management large image component', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('sets correct classes and styles if imageStyle is set', () => {
createComponent(
{
isLoading: false,
image: 'test.jpg',
name: 'test',
},
{
imageStyle: {
width: '100px',
height: '100px',
},
},
);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.element).toMatchSnapshot();
});
});
describe('zoom', () => {
beforeEach(() => {
createComponent(
{
isLoading: false,
image: 'test.jpg',
name: 'test',
},
{
imageStyle: {
width: '100px',
height: '100px',
},
baseImageSize: {
width: 100,
height: 100,
},
},
);
});
it('emits @resize event on zoom', () => {
wrapper.vm.zoom(2);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('resize')).toEqual([[{ width: 200, height: 200 }]]);
});
});
it('emits @resize event with base image size when scale=1', () => {
wrapper.vm.zoom(1);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('resize')).toEqual([[{ width: 100, height: 100 }]]);
});
});
it('sets image style when zoomed', () => {
wrapper.vm.zoom(2);
expect(wrapper.vm.imageStyle).toEqual({ width: '200px', height: '200px' });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.element).toMatchSnapshot();
});
});
});
});
......@@ -5,7 +5,7 @@ exports[`Design management design index page renders design index 1`] = `
class="design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row"
>
<div
class="d-flex overflow-hidden flex-lg-grow-1 flex-column"
class="d-flex overflow-hidden flex-grow-1 flex-column position-relative"
>
<design-destroyer-stub
filenames="test.jpg"
......@@ -19,7 +19,14 @@ exports[`Design management design index page renders design index 1`] = `
discussions="[object Object]"
image="test.jpg"
imagename="test.jpg"
scale="1"
/>
<div
class="design-scaler-wrapper position-absolute w-100 pb-4 d-flex-center"
>
<design-scaler-stub />
</div>
</div>
<div
......@@ -75,7 +82,7 @@ exports[`Design management design index page with error GlAlert is rendered in c
class="design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row"
>
<div
class="d-flex overflow-hidden flex-lg-grow-1 flex-column"
class="d-flex overflow-hidden flex-grow-1 flex-column position-relative"
>
<design-destroyer-stub
filenames="test.jpg"
......@@ -106,7 +113,14 @@ exports[`Design management design index page with error GlAlert is rendered in c
discussions=""
image="test.jpg"
imagename="test.jpg"
scale="1"
/>
<div
class="design-scaler-wrapper position-absolute w-100 pb-4 d-flex-center"
>
<design-scaler-stub />
</div>
</div>
<div
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment