Commit 4c9747b2 authored by Tom Quirk's avatar Tom Quirk Committed by Natalia Tepluhina

Test design management images when broken

- Test design list item image broken
- test design view image when broken
parent 8a2f29dd
<script> <script>
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { GlIcon } from '@gitlab/ui';
export default { export default {
components: {
GlIcon,
},
props: { props: {
image: { image: {
type: String, type: String,
...@@ -23,6 +27,7 @@ export default { ...@@ -23,6 +27,7 @@ export default {
return { return {
baseImageSize: null, baseImageSize: null,
imageStyle: null, imageStyle: null,
imageError: false,
}; };
}, },
watch: { watch: {
...@@ -49,6 +54,9 @@ export default { ...@@ -49,6 +54,9 @@ export default {
onImgLoad() { onImgLoad() {
requestIdleCallback(this.setBaseImageSize, { timeout: 1000 }); requestIdleCallback(this.setBaseImageSize, { timeout: 1000 });
}, },
onImgError() {
this.imageError = true;
},
setBaseImageSize() { setBaseImageSize() {
const { contentImg } = this.$refs; const { contentImg } = this.$refs;
if (!contentImg || contentImg.offsetHeight === 0 || contentImg.offsetWidth === 0) return; if (!contentImg || contentImg.offsetHeight === 0 || contentImg.offsetWidth === 0) return;
...@@ -86,13 +94,16 @@ export default { ...@@ -86,13 +94,16 @@ export default {
<template> <template>
<div class="m-auto js-design-image"> <div class="m-auto js-design-image">
<gl-icon v-if="imageError" class="text-secondary-100" name="media-broken" :size="48" />
<img <img
v-show="!imageError"
ref="contentImg" ref="contentImg"
class="mh-100" class="mh-100"
:src="image" :src="image"
:alt="name" :alt="name"
:style="imageStyle" :style="imageStyle"
:class="{ 'img-fluid': !imageStyle }" :class="{ 'img-fluid': !imageStyle }"
@error="onImgError"
@load="onImgLoad" @load="onImgLoad"
/> />
</div> </div>
......
<script> <script>
import { GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui'; import { GlLoadingIcon, GlIcon, GlIntersectionObserver } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue'; import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
import { n__, __ } from '~/locale'; import { n__, __ } from '~/locale';
...@@ -9,6 +9,7 @@ export default { ...@@ -9,6 +9,7 @@ export default {
components: { components: {
GlLoadingIcon, GlLoadingIcon,
GlIntersectionObserver, GlIntersectionObserver,
GlIcon,
Icon, Icon,
Timeago, Timeago,
}, },
...@@ -52,6 +53,7 @@ export default { ...@@ -52,6 +53,7 @@ export default {
data() { data() {
return { return {
imageLoading: true, imageLoading: true,
imageError: false,
isInView: false, isInView: false,
}; };
}, },
...@@ -81,16 +83,31 @@ export default { ...@@ -81,16 +83,31 @@ export default {
notesLabel() { notesLabel() {
return n__('%d comment', '%d comments', this.notesCount); return n__('%d comment', '%d comments', this.notesCount);
}, },
imageLink() {
return this.isInView ? this.imageV432x230 || this.image : '';
},
showLoadingSpinner() { showLoadingSpinner() {
return this.imageLoading || this.isUploading; return this.imageLoading || this.isUploading;
}, },
imageLink() { showImageErrorIcon() {
return this.isInView ? this.imageV432x230 || this.image : ''; return this.imageError && this.isInView;
},
showImage() {
return !this.showLoadingSpinner && !this.showImageErrorIcon;
}, },
}, },
methods: { methods: {
onImageLoad() { onImageLoad() {
this.imageLoading = false; this.imageLoading = false;
this.imageError = false;
},
onImageError() {
this.imageLoading = false;
this.imageError = true;
},
onAppear() {
this.isInView = true;
this.imageLoading = true;
}, },
}, },
DESIGN_ROUTE_NAME, DESIGN_ROUTE_NAME,
...@@ -112,15 +129,22 @@ export default { ...@@ -112,15 +129,22 @@ export default {
<icon :name="icon.name" :size="18" :class="icon.classes" /> <icon :name="icon.name" :size="18" :class="icon.classes" />
</span> </span>
</div> </div>
<gl-loading-icon v-show="showLoadingSpinner" size="md" /> <gl-intersection-observer @appear="onAppear">
<gl-intersection-observer @appear="isInView = true"> <gl-loading-icon v-if="showLoadingSpinner" size="md" />
<gl-icon
v-else-if="showImageErrorIcon"
name="media-broken"
class="text-secondary"
:size="32"
/>
<img <img
v-show="!showLoadingSpinner" v-show="showImage"
:src="imageLink" :src="imageLink"
:alt="filename" :alt="filename"
class="block mx-auto mw-100 mh-100 design-img" class="block mx-auto mw-100 mh-100 design-img"
data-qa-selector="design_image" data-qa-selector="design_image"
@load="onImageLoad" @load="onImageLoad"
@error="onImageError"
/> />
</gl-intersection-observer> </gl-intersection-observer>
</div> </div>
......
---
title: Show custom 'media broken' icon for broken images in Design Management
merge_request: 27460
author:
type: added
...@@ -4,6 +4,8 @@ exports[`Design management large image component renders image 1`] = ` ...@@ -4,6 +4,8 @@ exports[`Design management large image component renders image 1`] = `
<div <div
class="m-auto js-design-image" class="m-auto js-design-image"
> >
<!---->
<img <img
alt="test" alt="test"
class="mh-100 img-fluid" class="mh-100 img-fluid"
...@@ -17,6 +19,8 @@ exports[`Design management large image component renders loading state 1`] = ` ...@@ -17,6 +19,8 @@ exports[`Design management large image component renders loading state 1`] = `
class="m-auto js-design-image" class="m-auto js-design-image"
isloading="true" isloading="true"
> >
<!---->
<img <img
alt="" alt=""
class="mh-100 img-fluid" class="mh-100 img-fluid"
...@@ -25,10 +29,20 @@ exports[`Design management large image component renders loading state 1`] = ` ...@@ -25,10 +29,20 @@ exports[`Design management large image component renders loading state 1`] = `
</div> </div>
`; `;
exports[`Design management large image component renders media broken icon on error 1`] = `
<gl-icon-stub
class="text-secondary-100"
name="media-broken"
size="48"
/>
`;
exports[`Design management large image component sets correct classes and styles if imageStyle is set 1`] = ` exports[`Design management large image component sets correct classes and styles if imageStyle is set 1`] = `
<div <div
class="m-auto js-design-image" class="m-auto js-design-image"
> >
<!---->
<img <img
alt="test" alt="test"
class="mh-100" class="mh-100"
...@@ -42,6 +56,8 @@ exports[`Design management large image component zoom sets image style when zoom ...@@ -42,6 +56,8 @@ exports[`Design management large image component zoom sets image style when zoom
<div <div
class="m-auto js-design-image" class="m-auto js-design-image"
> >
<!---->
<img <img
alt="test" alt="test"
class="mh-100" class="mh-100"
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import DesignImage from 'ee/design_management/components/image.vue'; import DesignImage from 'ee/design_management/components/image.vue';
describe('Design management large image component', () => { describe('Design management large image component', () => {
...@@ -52,6 +53,21 @@ describe('Design management large image component', () => { ...@@ -52,6 +53,21 @@ describe('Design management large image component', () => {
}); });
}); });
it('renders media broken icon on error', () => {
createComponent({
isLoading: false,
image: 'test.jpg',
name: 'test',
});
const image = wrapper.find('img');
image.trigger('error');
return wrapper.vm.$nextTick().then(() => {
expect(image.isVisible()).toBe(false);
expect(wrapper.find(GlIcon).element).toMatchSnapshot();
});
});
describe('zoom', () => { describe('zoom', () => {
const baseImageWidth = 100; const baseImageWidth = 100;
const baseImageHeight = 100; const baseImageHeight = 100;
...@@ -75,12 +91,10 @@ describe('Design management large image component', () => { ...@@ -75,12 +91,10 @@ describe('Design management large image component', () => {
}, },
); );
jest jest.spyOn(wrapper.vm.$refs.contentImg, 'offsetWidth', 'get').mockReturnValue(baseImageWidth);
.spyOn(wrapper.vm.$refs.contentImg, 'offsetWidth', 'get')
.mockImplementation(() => baseImageWidth);
jest jest
.spyOn(wrapper.vm.$refs.contentImg, 'offsetHeight', 'get') .spyOn(wrapper.vm.$refs.contentImg, 'offsetHeight', 'get')
.mockImplementation(() => baseImageHeight); .mockReturnValue(baseImageHeight);
}); });
it('emits @resize event on zoom', () => { it('emits @resize event on zoom', () => {
......
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management list item component when item appears in view renders media broken icon when image onerror triggered 1`] = `
<gl-icon-stub
class="text-secondary"
name="media-broken"
size="32"
/>
`;
exports[`Design management list item component with no notes renders item with correct status icon for creation event 1`] = ` exports[`Design management list item component with no notes renders item with correct status icon for creation event 1`] = `
<router-link-stub <router-link-stub
class="card cursor-pointer text-plain js-design-list-item design-list-item" class="card cursor-pointer text-plain js-design-list-item design-list-item"
...@@ -23,16 +31,11 @@ exports[`Design management list item component with no notes renders item with c ...@@ -23,16 +31,11 @@ exports[`Design management list item component with no notes renders item with c
</span> </span>
</div> </div>
<gl-loading-icon-stub
color="orange"
label="Loading"
size="md"
style="display: none;"
/>
<gl-intersection-observer-stub <gl-intersection-observer-stub
options="[object Object]" options="[object Object]"
> >
<!---->
<img <img
alt="test" alt="test"
class="block mx-auto mw-100 mh-100 design-img" class="block mx-auto mw-100 mh-100 design-img"
...@@ -96,16 +99,11 @@ exports[`Design management list item component with no notes renders item with c ...@@ -96,16 +99,11 @@ exports[`Design management list item component with no notes renders item with c
</span> </span>
</div> </div>
<gl-loading-icon-stub
color="orange"
label="Loading"
size="md"
style="display: none;"
/>
<gl-intersection-observer-stub <gl-intersection-observer-stub
options="[object Object]" options="[object Object]"
> >
<!---->
<img <img
alt="test" alt="test"
class="block mx-auto mw-100 mh-100 design-img" class="block mx-auto mw-100 mh-100 design-img"
...@@ -169,16 +167,11 @@ exports[`Design management list item component with no notes renders item with c ...@@ -169,16 +167,11 @@ exports[`Design management list item component with no notes renders item with c
</span> </span>
</div> </div>
<gl-loading-icon-stub
color="orange"
label="Loading"
size="md"
style="display: none;"
/>
<gl-intersection-observer-stub <gl-intersection-observer-stub
options="[object Object]" options="[object Object]"
> >
<!---->
<img <img
alt="test" alt="test"
class="block mx-auto mw-100 mh-100 design-img" class="block mx-auto mw-100 mh-100 design-img"
...@@ -229,16 +222,11 @@ exports[`Design management list item component with no notes renders item with n ...@@ -229,16 +222,11 @@ exports[`Design management list item component with no notes renders item with n
> >
<!----> <!---->
<gl-loading-icon-stub
color="orange"
label="Loading"
size="md"
style="display: none;"
/>
<gl-intersection-observer-stub <gl-intersection-observer-stub
options="[object Object]" options="[object Object]"
> >
<!---->
<img <img
alt="test" alt="test"
class="block mx-auto mw-100 mh-100 design-img" class="block mx-auto mw-100 mh-100 design-img"
...@@ -289,15 +277,15 @@ exports[`Design management list item component with no notes renders loading spi ...@@ -289,15 +277,15 @@ exports[`Design management list item component with no notes renders loading spi
> >
<!----> <!---->
<gl-intersection-observer-stub
options="[object Object]"
>
<gl-loading-icon-stub <gl-loading-icon-stub
color="orange" color="orange"
label="Loading" label="Loading"
size="md" size="md"
/> />
<gl-intersection-observer-stub
options="[object Object]"
>
<img <img
alt="test" alt="test"
class="block mx-auto mw-100 mh-100 design-img" class="block mx-auto mw-100 mh-100 design-img"
...@@ -349,16 +337,11 @@ exports[`Design management list item component with notes renders item with mult ...@@ -349,16 +337,11 @@ exports[`Design management list item component with notes renders item with mult
> >
<!----> <!---->
<gl-loading-icon-stub
color="orange"
label="Loading"
size="md"
style="display: none;"
/>
<gl-intersection-observer-stub <gl-intersection-observer-stub
options="[object Object]" options="[object Object]"
> >
<!---->
<img <img
alt="test" alt="test"
class="block mx-auto mw-100 mh-100 design-img" class="block mx-auto mw-100 mh-100 design-img"
...@@ -426,16 +409,11 @@ exports[`Design management list item component with notes renders item with sing ...@@ -426,16 +409,11 @@ exports[`Design management list item component with notes renders item with sing
> >
<!----> <!---->
<gl-loading-icon-stub
color="orange"
label="Loading"
size="md"
style="display: none;"
/>
<gl-intersection-observer-stub <gl-intersection-observer-stub
options="[object Object]" options="[object Object]"
> >
<!---->
<img <img
alt="test" alt="test"
class="block mx-auto mw-100 mh-100 design-img" class="block mx-auto mw-100 mh-100 design-img"
......
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlIcon, GlIntersectionObserver } from '@gitlab/ui';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import Item from 'ee/design_management/components/list/item.vue'; import Item from 'ee/design_management/components/list/item.vue';
import { GlIntersectionObserver } from '@gitlab/ui';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueRouter); localVue.use(VueRouter);
...@@ -59,25 +59,36 @@ describe('Design management list item component', () => { ...@@ -59,25 +59,36 @@ describe('Design management list item component', () => {
}); });
describe('when item appears in view', () => { describe('when item appears in view', () => {
let image;
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
image = wrapper.find('img');
expect(image.attributes('src')).toBe('');
wrapper.find(GlIntersectionObserver).vm.$emit('appear'); wrapper.find(GlIntersectionObserver).vm.$emit('appear');
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
it('renders an image', () => { it('renders an image', () => {
const image = wrapper.find('img');
expect(image.attributes('src')).toBe('http://via.placeholder.com/300'); expect(image.attributes('src')).toBe('http://via.placeholder.com/300');
}); });
it('renders media broken icon when image onerror triggered', () => {
image.trigger('error');
return wrapper.vm.$nextTick().then(() => {
expect(image.isVisible()).toBe(false);
expect(wrapper.find(GlIcon).element).toMatchSnapshot();
});
});
describe('when imageV432x230 and image provided', () => { describe('when imageV432x230 and image provided', () => {
it('renders imageV432x230 image', () => { it('renders imageV432x230 image', () => {
const mockSrc = 'mock-imageV432x230-url'; const mockSrc = 'mock-imageV432x230-url';
wrapper.setProps({ imageV432x230: mockSrc }); wrapper.setProps({ imageV432x230: mockSrc });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
const image = wrapper.find('img');
expect(image.attributes('src')).toBe(mockSrc); expect(image.attributes('src')).toBe(mockSrc);
}); });
}); });
......
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