mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
Handle user avatar error (#3735)
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
106
src/components/common/UserAvatar.test.ts
Normal file
106
src/components/common/UserAvatar.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
25
src/components/common/UserAvatar.vue
Normal file
25
src/components/common/UserAvatar.vue
Normal 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>
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1236,6 +1236,7 @@
|
||||
"passwordResetSent": "Password reset email sent",
|
||||
"passwordResetSentDetail": "Please check your email for a link to reset your password.",
|
||||
"newUser": "New here?",
|
||||
"userAvatar": "User Avatar",
|
||||
"signUp": "Sign up",
|
||||
"emailLabel": "Email",
|
||||
"emailPlaceholder": "Enter your email",
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
"success": "Inicio de sesión exitoso",
|
||||
"termsLink": "Términos de uso",
|
||||
"termsText": "Al hacer clic en \"Siguiente\" o \"Registrarse\", aceptas nuestros",
|
||||
"title": "Inicia sesión en tu cuenta"
|
||||
"title": "Inicia sesión en tu cuenta",
|
||||
"userAvatar": "Avatar de usuario"
|
||||
},
|
||||
"passwordUpdate": {
|
||||
"success": "Contraseña actualizada",
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
"success": "Connexion réussie",
|
||||
"termsLink": "Conditions d'utilisation",
|
||||
"termsText": "En cliquant sur \"Suivant\" ou \"S'inscrire\", vous acceptez nos",
|
||||
"title": "Connectez-vous à votre compte"
|
||||
"title": "Connectez-vous à votre compte",
|
||||
"userAvatar": "Avatar utilisateur"
|
||||
},
|
||||
"passwordUpdate": {
|
||||
"success": "Mot de passe mis à jour",
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
"success": "ログイン成功",
|
||||
"termsLink": "利用規約",
|
||||
"termsText": "「次へ」または「サインアップ」をクリックすると、私たちの",
|
||||
"title": "アカウントにログインする"
|
||||
"title": "アカウントにログインする",
|
||||
"userAvatar": "ユーザーアバター"
|
||||
},
|
||||
"passwordUpdate": {
|
||||
"success": "パスワードが更新されました",
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
"success": "로그인 성공",
|
||||
"termsLink": "이용 약관",
|
||||
"termsText": "\"다음\" 또는 \"가입하기\"를 클릭하면 우리의",
|
||||
"title": "계정에 로그인"
|
||||
"title": "계정에 로그인",
|
||||
"userAvatar": "사용자 아바타"
|
||||
},
|
||||
"passwordUpdate": {
|
||||
"success": "비밀번호가 업데이트되었습니다",
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
"success": "Вход выполнен успешно",
|
||||
"termsLink": "Условиями использования",
|
||||
"termsText": "Нажимая \"Далее\" или \"Зарегистрироваться\", вы соглашаетесь с нашими",
|
||||
"title": "Войдите в свой аккаунт"
|
||||
"title": "Войдите в свой аккаунт",
|
||||
"userAvatar": "Аватар пользователя"
|
||||
},
|
||||
"passwordUpdate": {
|
||||
"success": "Пароль обновлён",
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
"success": "登录成功",
|
||||
"termsLink": "使用条款",
|
||||
"termsText": "点击“下一步”或“注册”即表示您同意我们的",
|
||||
"title": "登录您的账户"
|
||||
"title": "登录您的账户",
|
||||
"userAvatar": "用户头像"
|
||||
},
|
||||
"passwordUpdate": {
|
||||
"success": "密码已更新",
|
||||
|
||||
Reference in New Issue
Block a user