Handle user avatar error (#3735)

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Christian Byrne
2025-05-06 19:04:25 -07:00
committed by GitHub
parent 754eb807de
commit bbbf140b1f
12 changed files with 155 additions and 23 deletions

View File

@@ -0,0 +1,106 @@
import { mount } from '@vue/test-utils'
import Avatar from 'primevue/avatar'
import PrimeVue from 'primevue/config'
import { beforeEach, describe, expect, it } from 'vitest'
import { createApp, nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
import UserAvatar from './UserAvatar.vue'
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
auth: {
login: {
userAvatar: 'User Avatar'
}
}
}
}
})
describe('UserAvatar', () => {
beforeEach(() => {
const app = createApp({})
app.use(PrimeVue)
})
const mountComponent = (props: any = {}) => {
return mount(UserAvatar, {
global: {
plugins: [PrimeVue, i18n],
components: { Avatar }
},
props
})
}
it('renders correctly with photo Url', async () => {
const wrapper = mountComponent({
photoUrl: 'https://example.com/avatar.jpg'
})
const avatar = wrapper.findComponent(Avatar)
expect(avatar.exists()).toBe(true)
expect(avatar.props('image')).toBe('https://example.com/avatar.jpg')
expect(avatar.props('icon')).toBeNull()
})
it('renders with default icon when no photo Url is provided', () => {
const wrapper = mountComponent({
photoUrl: undefined
})
const avatar = wrapper.findComponent(Avatar)
expect(avatar.exists()).toBe(true)
expect(avatar.props('image')).toBeNull()
expect(avatar.props('icon')).toBe('pi pi-user')
})
it('renders with default icon when provided photo Url is null', () => {
const wrapper = mountComponent({
photoUrl: null
})
const avatar = wrapper.findComponent(Avatar)
expect(avatar.exists()).toBe(true)
expect(avatar.props('image')).toBeNull()
expect(avatar.props('icon')).toBe('pi pi-user')
})
it('falls back to icon when image fails to load', async () => {
const wrapper = mountComponent({
photoUrl: 'https://example.com/broken-image.jpg'
})
const avatar = wrapper.findComponent(Avatar)
expect(avatar.props('icon')).toBeNull()
// Simulate image load error
avatar.vm.$emit('error')
await nextTick()
expect(avatar.props('icon')).toBe('pi pi-user')
})
it('uses provided ariaLabel', () => {
const wrapper = mountComponent({
photoUrl: 'https://example.com/avatar.jpg',
ariaLabel: 'Custom Label'
})
const avatar = wrapper.findComponent(Avatar)
expect(avatar.attributes('aria-label')).toBe('Custom Label')
})
it('falls back to i18n translation when no ariaLabel is provided', () => {
const wrapper = mountComponent({
photoUrl: 'https://example.com/avatar.jpg'
})
const avatar = wrapper.findComponent(Avatar)
expect(avatar.attributes('aria-label')).toBe('User Avatar')
})
})

View File

@@ -0,0 +1,25 @@
<template>
<Avatar
:image="photoUrl ?? undefined"
:icon="hasAvatar ? undefined : 'pi pi-user'"
shape="circle"
:aria-label="ariaLabel ?? $t('auth.login.userAvatar')"
@error="handleImageError"
/>
</template>
<script setup lang="ts">
import Avatar from 'primevue/avatar'
import { computed, ref } from 'vue'
const { photoUrl, ariaLabel } = defineProps<{
photoUrl?: string | null
ariaLabel?: string
}>()
const imageError = ref(false)
const handleImageError = () => {
imageError.value = true
}
const hasAvatar = computed(() => photoUrl && !imageError.value)
</script>

View File

@@ -5,12 +5,11 @@
<Divider class="mb-3" />
<div v-if="user" class="flex flex-col gap-2">
<Avatar
<UserAvatar
v-if="user.photoURL"
:image="user.photoURL"
:photo-url="user.photoURL"
shape="circle"
size="large"
aria-label="User Avatar"
/>
<div class="flex flex-col gap-0.5">
@@ -87,13 +86,13 @@
</template>
<script setup lang="ts">
import Avatar from 'primevue/avatar'
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import ProgressSpinner from 'primevue/progressspinner'
import TabPanel from 'primevue/tabpanel'
import { computed } from 'vue'
import UserAvatar from '@/components/common/UserAvatar.vue'
import { useDialogService } from '@/services/dialogService'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'

View File

@@ -12,12 +12,7 @@
<div
class="flex items-center rounded-full bg-[var(--p-content-background)]"
>
<Avatar
:image="photoURL"
:icon="photoURL ? undefined : 'pi pi-user'"
shape="circle"
aria-label="User Avatar"
/>
<UserAvatar :photo-url="photoURL" />
<i class="pi pi-chevron-down px-1" :style="{ fontSize: '0.5rem' }" />
</div>
@@ -30,11 +25,11 @@
</template>
<script setup lang="ts">
import Avatar from 'primevue/avatar'
import Button from 'primevue/button'
import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
import UserAvatar from '@/components/common/UserAvatar.vue'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import CurrentUserPopover from './CurrentUserPopover.vue'

View File

@@ -4,13 +4,13 @@
<!-- User Info Section -->
<div class="p-3">
<div class="flex flex-col items-center">
<Avatar
<UserAvatar
class="mb-3"
:image="user?.photoURL ?? undefined"
:icon="user?.photoURL ? undefined : 'pi pi-user !text-2xl'"
shape="circle"
:photo-url="user?.photoURL"
:pt:icon:class="{
'!text-2xl': !user?.photoURL
}"
size="large"
aria-label="User Avatar"
/>
<!-- User Details -->
@@ -50,11 +50,11 @@
</template>
<script setup lang="ts">
import Avatar from 'primevue/avatar'
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import { computed, onMounted } from 'vue'
import UserAvatar from '@/components/common/UserAvatar.vue'
import UserCredit from '@/components/common/UserCredit.vue'
import { useDialogService } from '@/services/dialogService'
import { useFirebaseAuthService } from '@/services/firebaseAuthService'