Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion src/modules/core/components/user.avatar.component.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
>
<v-img
v-if="hasAvatar"
:src="setImages(config.api, user.avatar, size * 2, null)"
:src="setImages(config.api, user.avatar, avatarSize, null)"
:alt="fullName"
/>
<span v-else :class="textColorClass" :style="{ fontSize: `${Math.max(size * 0.4, 10)}px`, fontWeight: 500 }">
Expand All @@ -31,6 +31,15 @@
* Color is derived from email hash for consistency.
*/

/**
* Allowed avatar sizes served by the backend `/api/uploads/images/` endpoint.
* Mirrors the sharp-resize whitelist in devkit Node
* `modules/uploads/config/uploads.{env}.config.js` (field `uploads.avatar.sharp.sizes`).
* Any value outside this list returns HTTP 422 "Wrong size param" — if you
* change one, update the other.
*/
const SHARP_SIZES = [128, 256, 512, 1024];

/**
* Determine whether a hex color is light (needs dark text for contrast).
* Uses relative luminance per WCAG 2.x.
Expand Down Expand Up @@ -80,6 +89,16 @@ export default {
hasAvatar() {
return !!(this.user && this.user.avatar && this.user.avatar !== '');
},
/**
* Snap the requested avatar size (doubled for retina) to the backend
* sharp whitelist. Any value outside `SHARP_SIZES` would return HTTP 422,
* so we pick the smallest allowed size that fits — or cap at the largest.
* @returns {number} A size from `SHARP_SIZES` (128, 256, 512, or 1024).
*/
avatarSize() {
const requested = this.size * 2;
return SHARP_SIZES.find((s) => s >= requested) ?? SHARP_SIZES[SHARP_SIZES.length - 1];
},
Comment thread
PierreBrisorgueil marked this conversation as resolved.
initials() {
if (!this.user) return '';
const f = (this.user.firstName || '').charAt(0).toUpperCase();
Expand Down
43 changes: 42 additions & 1 deletion src/modules/core/tests/user.avatar.component.unit.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import * as components from 'vuetify/components';
import * as directives from 'vuetify/directives';
import { describe, it, expect, beforeEach } from 'vitest';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import UserAvatarComponent from '../components/user.avatar.component.vue';

const mockConfig = {
Expand Down Expand Up @@ -86,6 +86,47 @@ describe('user.avatar.component', () => {
});
});

describe('avatarSize computed', () => {
it('snaps size=24 (requested 48) to 128', () => {
const wrapper = createWrapper({ user: { email: 'a@b.com' }, size: 24 });
expect(wrapper.vm.avatarSize).toBe(128);
});

it('snaps size=64 (requested 128) to 128', () => {
const wrapper = createWrapper({ user: { email: 'a@b.com' }, size: 64 });
expect(wrapper.vm.avatarSize).toBe(128);
});

it('snaps size=200 (requested 400) to 512', () => {
const wrapper = createWrapper({ user: { email: 'a@b.com' }, size: 200 });
expect(wrapper.vm.avatarSize).toBe(512);
});

it('snaps size=512 (requested 1024) to 1024', () => {
const wrapper = createWrapper({ user: { email: 'a@b.com' }, size: 512 });
expect(wrapper.vm.avatarSize).toBe(1024);
});

it('caps size=600 (requested 1200) to 1024', () => {
const wrapper = createWrapper({ user: { email: 'a@b.com' }, size: 600 });
expect(wrapper.vm.avatarSize).toBe(1024);
});

it('uses snapped size when building avatar image URL', () => {
const setImages = vi.fn((api, file, size) => `${api.protocol}://${api.host}:${api.port}/api/uploads/images/${file}-${size}`);
const wrapper = mount(UserAvatarComponent, {
props: { user: { email: 'a@b.com', avatar: 'photo.jpg' }, size: 200 },
global: {
plugins: [vuetify],
config: { globalProperties: { config: mockConfig, setImages } },
},
});

expect(wrapper.vm.avatarSize).toBe(512);
expect(setImages).toHaveBeenCalledWith(mockConfig.api, 'photo.jpg', 512, null);
});
});

Comment thread
coderabbitai[bot] marked this conversation as resolved.
describe('hasAvatar computed', () => {
it('returns true when user has avatar', () => {
const wrapper = createWrapper({ user: { avatar: 'photo.jpg', email: 'a@b.com' } });
Expand Down
Loading