Commit 62ad1a2f authored by Frédéric Caplette's avatar Frédéric Caplette

Merge branch '352508-gl-avatars-for-all-user-avatars' into 'master'

Render user avatar image using `GlAvatar`

See merge request gitlab-org/gitlab!81437
parents efaec9fc a231a01b
<script>
/* This is a re-usable vue component for rendering a user avatar that
does not need to link to the user's profile. The image and an optional
tooltip can be configured by props passed to this component.
does not need to link to the user's profile. The image and an optional
tooltip can be configured by props passed to this component.
Sample configuration:
Sample configuration:
<user-avatar-image
:lazy="true"
:img-src="userAvatarSrc"
:img-alt="tooltipText"
:tooltip-text="tooltipText"
tooltip-placement="top"
/>
<user-avatar-image
lazy
:img-src="userAvatarSrc"
:img-alt="tooltipText"
:tooltip-text="tooltipText"
tooltip-placement="top"
/>
*/
*/
import { GlTooltip } from '@gitlab/ui';
import { GlTooltip, GlAvatar } from '@gitlab/ui';
import defaultAvatarUrl from 'images/no_avatar.png';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { placeholderImage } from '../../../lazy_loader';
export default {
name: 'UserAvatarImage',
components: {
GlTooltip,
GlAvatar,
},
mixins: [glFeatureFlagMixin()],
props: {
lazy: {
type: Boolean,
......@@ -85,7 +88,20 @@ export default {
<template>
<span>
<gl-avatar
v-if="glFeatures.glAvatarForAllUserAvatars"
ref="userAvatarImage"
:class="{
lazy: lazy,
[cssClasses]: true,
}"
:src="resultantSrcAttribute"
:data-src="sanitizedSource"
:size="size"
:alt="imgAlt"
/>
<img
v-else
ref="userAvatarImage"
:class="{
lazy: lazy,
......@@ -100,11 +116,9 @@ export default {
class="avatar"
/>
<gl-tooltip
v-if="tooltipText || $slots.default"
:target="() => $refs.userAvatarImage"
:placement="tooltipPlacement"
boundary="window"
class="js-user-avatar-image-tooltip"
>
<slot> {{ tooltipText }} </slot>
</gl-tooltip>
......
---
name: gl_avatar_for_all_user_avatars
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81437
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353477
milestone: '14.9'
type: development
group: group::foundations
default_enabled: false
......@@ -17,6 +17,7 @@ RSpec.describe 'Merge Requests > User resets approvers', :js do
before do
stub_licensed_features(multiple_approval_rules: true)
stub_feature_flags(gl_avatar_for_all_user_avatars: false)
project_approvers.each do |approver|
project.add_developer(approver)
......
......@@ -18,6 +18,7 @@ RSpec.describe 'Project settings > [EE] Merge Request Approvals', :js do
project.add_maintainer(user)
group.add_developer(user)
group.add_developer(group_member)
stub_feature_flags(gl_avatar_for_all_user_avatars: false)
end
it 'adds approver' do
......
......@@ -59,6 +59,7 @@ module Gitlab
push_frontend_feature_flag(:bootstrap_confirmation_modals, default_enabled: :yaml)
push_frontend_feature_flag(:sandboxed_mermaid, default_enabled: :yaml)
push_frontend_feature_flag(:source_editor_toolbar, default_enabled: :yaml)
push_frontend_feature_flag(:gl_avatar_for_all_user_avatars, default_enabled: :yaml)
end
# Exposes the state of a feature flag to the frontend code.
......
......@@ -23,6 +23,7 @@ RSpec.describe 'Project issue boards', :js do
project.add_maintainer(user2)
sign_in(user)
stub_feature_flags(gl_avatar_for_all_user_avatars: false)
set_cookie('sidebar_collapsed', 'true')
end
......
......@@ -25,6 +25,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
before do
project.add_maintainer(user)
sign_in user
stub_feature_flags(gl_avatar_for_all_user_avatars: false)
set_cookie('sidebar_collapsed', 'true')
end
......
import { shallowMount } from '@vue/test-utils';
import { GlAvatar, GlTooltip } from '@gitlab/ui';
import defaultAvatarUrl from 'images/no_avatar.png';
import { placeholderImage } from '~/lazy_loader';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
jest.mock('images/no_avatar.png', () => 'default-avatar-url');
const DEFAULT_PROPS = {
size: 99,
const PROVIDED_PROPS = {
size: 32,
imgSrc: 'myavatarurl.com',
imgAlt: 'mydisplayname',
cssClasses: 'myextraavatarclass',
......@@ -14,6 +15,10 @@ const DEFAULT_PROPS = {
tooltipPlacement: 'bottom',
};
const DEFAULT_PROPS = {
size: 20,
};
describe('User Avatar Image Component', () => {
let wrapper;
......@@ -21,64 +26,149 @@ describe('User Avatar Image Component', () => {
wrapper.destroy();
});
describe('Initialization', () => {
beforeEach(() => {
wrapper = shallowMount(UserAvatarImage, {
propsData: {
...DEFAULT_PROPS,
},
describe('`glAvatarForAllUserAvatars` feature flag enabled', () => {
describe('Initialization', () => {
beforeEach(() => {
wrapper = shallowMount(UserAvatarImage, {
propsData: {
...PROVIDED_PROPS,
},
provide: {
glFeatures: {
glAvatarForAllUserAvatars: true,
},
},
});
});
it('should render `GlAvatar` and provide correct properties to it', () => {
const avatar = wrapper.findComponent(GlAvatar);
expect(avatar.attributes('data-src')).toBe(
`${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
);
expect(avatar.props()).toMatchObject({
src: `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
alt: PROVIDED_PROPS.imgAlt,
});
});
it('should add correct CSS classes', () => {
const classes = wrapper.findComponent(GlAvatar).classes();
expect(classes).toContain(PROVIDED_PROPS.cssClasses);
expect(classes).not.toContain('lazy');
});
});
it('should have <img> as a child element', () => {
const imageElement = wrapper.find('img');
describe('Initialization when lazy', () => {
beforeEach(() => {
wrapper = shallowMount(UserAvatarImage, {
propsData: {
...PROVIDED_PROPS,
lazy: true,
},
provide: {
glFeatures: {
glAvatarForAllUserAvatars: true,
},
},
});
});
it('should add lazy attributes', () => {
const avatar = wrapper.findComponent(GlAvatar);
expect(imageElement.exists()).toBe(true);
expect(imageElement.attributes('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
expect(imageElement.attributes('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
expect(imageElement.attributes('alt')).toBe(DEFAULT_PROPS.imgAlt);
expect(avatar.classes()).toContain('lazy');
expect(avatar.attributes()).toMatchObject({
src: placeholderImage,
'data-src': `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
});
});
});
it('should properly render img css', () => {
const classes = wrapper.find('img').classes();
expect(classes).toEqual(expect.arrayContaining(['avatar', 's99', DEFAULT_PROPS.cssClasses]));
expect(classes).not.toContain('lazy');
describe('Initialization without src', () => {
beforeEach(() => {
wrapper = shallowMount(UserAvatarImage);
});
it('should have default avatar image', () => {
const imageElement = wrapper.find('img');
expect(imageElement.attributes('src')).toBe(
`${defaultAvatarUrl}?width=${DEFAULT_PROPS.size}`,
);
});
});
});
describe('Initialization when lazy', () => {
beforeEach(() => {
wrapper = shallowMount(UserAvatarImage, {
propsData: {
...DEFAULT_PROPS,
lazy: true,
},
describe('`glAvatarForAllUserAvatars` feature flag disabled', () => {
describe('Initialization', () => {
beforeEach(() => {
wrapper = shallowMount(UserAvatarImage, {
propsData: {
...PROVIDED_PROPS,
},
});
});
});
it('should add lazy attributes', () => {
const imageElement = wrapper.find('img');
it('should have <img> as a child element', () => {
const imageElement = wrapper.find('img');
expect(imageElement.exists()).toBe(true);
expect(imageElement.attributes('src')).toBe(
`${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
);
expect(imageElement.attributes('data-src')).toBe(
`${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
);
expect(imageElement.attributes('alt')).toBe(PROVIDED_PROPS.imgAlt);
});
expect(imageElement.classes()).toContain('lazy');
expect(imageElement.attributes('src')).toBe(placeholderImage);
expect(imageElement.attributes('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
it('should properly render img css', () => {
const classes = wrapper.find('img').classes();
expect(classes).toEqual(['avatar', 's32', PROVIDED_PROPS.cssClasses]);
expect(classes).not.toContain('lazy');
});
});
});
describe('Initialization without src', () => {
beforeEach(() => {
wrapper = shallowMount(UserAvatarImage);
describe('Initialization when lazy', () => {
beforeEach(() => {
wrapper = shallowMount(UserAvatarImage, {
propsData: {
...PROVIDED_PROPS,
lazy: true,
},
});
});
it('should add lazy attributes', () => {
const imageElement = wrapper.find('img');
expect(imageElement.classes()).toContain('lazy');
expect(imageElement.attributes('src')).toBe(placeholderImage);
expect(imageElement.attributes('data-src')).toBe(
`${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
);
});
});
it('should have default avatar image', () => {
const imageElement = wrapper.find('img');
describe('Initialization without src', () => {
beforeEach(() => {
wrapper = shallowMount(UserAvatarImage);
});
it('should have default avatar image', () => {
const imageElement = wrapper.find('img');
expect(imageElement.attributes('src')).toBe(`${defaultAvatarUrl}?width=20`);
expect(imageElement.attributes('src')).toBe(
`${defaultAvatarUrl}?width=${DEFAULT_PROPS.size}`,
);
});
});
});
describe('dynamic tooltip content', () => {
const props = DEFAULT_PROPS;
const props = PROVIDED_PROPS;
const slots = {
default: ['Action!'],
};
......@@ -91,11 +181,11 @@ describe('User Avatar Image Component', () => {
});
it('renders the tooltip slot', () => {
expect(wrapper.find('.js-user-avatar-image-tooltip').exists()).toBe(true);
expect(wrapper.findComponent(GlTooltip).exists()).toBe(true);
});
it('renders the tooltip content', () => {
expect(wrapper.find('.js-user-avatar-image-tooltip').text()).toContain(slots.default[0]);
expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]);
});
it('does not render tooltip data attributes for on avatar image', () => {
......
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