From 97cf26082fa41b9288b7356a00f47125494d02cf Mon Sep 17 00:00:00 2001 From: RainbowBird Date: Wed, 29 Jul 2026 18:38:37 +0800 Subject: [PATCH] refactor(ui): share user avatar fallbacks (#2143) --- .../controls-island-auth-button.test.ts | 83 +++++++++++++++++++ .../controls-island-auth-button.vue | 14 +--- apps/ui-server-auth/src/pages/profile.vue | 17 ++-- docs/ai/context/ui-components.md | 14 ++++ .../src/components/Layouts/HeaderAvatar.vue | 34 +++----- .../account/account-settings-page.vue | 24 ++---- packages/ui/src/components/misc/avatar.vue | 58 +++++++++++++ packages/ui/src/components/misc/index.ts | 1 + 8 files changed, 182 insertions(+), 63 deletions(-) create mode 100644 apps/stage-tamagotchi/src/renderer/components/stage-islands/controls-island/controls-island-auth-button.test.ts create mode 100644 packages/ui/src/components/misc/avatar.vue diff --git a/apps/stage-tamagotchi/src/renderer/components/stage-islands/controls-island/controls-island-auth-button.test.ts b/apps/stage-tamagotchi/src/renderer/components/stage-islands/controls-island/controls-island-auth-button.test.ts new file mode 100644 index 000000000..0d3edd917 --- /dev/null +++ b/apps/stage-tamagotchi/src/renderer/components/stage-islands/controls-island/controls-island-auth-button.test.ts @@ -0,0 +1,83 @@ +// @vitest-environment jsdom + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { createApp, h, nextTick, ref } from 'vue' + +import ControlsIslandAuthButton from './controls-island-auth-button.vue' + +const authState = { + isAuthenticated: ref(true), + user: ref<{ name: string, image?: string }>({ + name: 'Rainbow Bird', + image: 'https://example.com/broken-avatar.png', + }), + needsLogin: ref(false), + credits: ref(9620), +} + +vi.mock('@proj-airi/stage-ui/stores/auth', () => ({ + useAuthStore: () => authState, +})) + +vi.mock('@proj-airi/electron-vueuse', () => ({ + useElectronEventaContext: () => ref({ + on: vi.fn(), + }), + useElectronEventaInvoke: () => vi.fn(), +})) + +vi.mock('pinia', () => ({ + storeToRefs: (store: object) => store, +})) + +vi.mock('vue-i18n', () => ({ + useI18n: () => ({ + t: (key: string) => key, + }), +})) + +describe('controlsIslandAuthButton', () => { + const mountedApps: Array<{ app: ReturnType, host: HTMLElement }> = [] + + afterEach(() => { + for (const { app, host } of mountedApps) { + app.unmount() + host.remove() + } + mountedApps.length = 0 + authState.user.value.image = 'https://example.com/broken-avatar.png' + }) + + function mountComponent() { + const host = document.createElement('div') + document.body.appendChild(host) + const app = createApp({ + render: () => h(ControlsIslandAuthButton), + }) + app.mount(host) + mountedApps.push({ app, host }) + return host + } + + it('renders the shared account fallback when no avatar is available', () => { + authState.user.value.image = undefined + const host = mountComponent() + + const fallback = host.querySelector('[data-avatar-fallback]') + expect(fallback).toBeTruthy() + expect(fallback?.firstElementChild?.classList.contains('i-solar:user-circle-bold-duotone')).toBe(true) + }) + + it('tries the next avatar URL after the authenticated user changes', async () => { + const host = mountComponent() + const previousImage = host.querySelector('[data-avatar-image]') + + authState.user.value.image = 'https://example.com/new-avatar.png' + await nextTick() + + const nextImage = host.querySelector('[data-avatar-image]') + expect(nextImage).not.toBe(previousImage) + expect(nextImage?.getAttribute('src')).toBe('https://example.com/new-avatar.png') + expect(nextImage?.getAttribute('alt')).toBe('') + }) +}) diff --git a/apps/stage-tamagotchi/src/renderer/components/stage-islands/controls-island/controls-island-auth-button.vue b/apps/stage-tamagotchi/src/renderer/components/stage-islands/controls-island/controls-island-auth-button.vue index a1b4ae2b6..d64b1fc97 100644 --- a/apps/stage-tamagotchi/src/renderer/components/stage-islands/controls-island/controls-island-auth-button.vue +++ b/apps/stage-tamagotchi/src/renderer/components/stage-islands/controls-island/controls-island-auth-button.vue @@ -1,6 +1,7 @@ + + diff --git a/packages/ui/src/components/misc/index.ts b/packages/ui/src/components/misc/index.ts index 31fb96590..790c04841 100644 --- a/packages/ui/src/components/misc/index.ts +++ b/packages/ui/src/components/misc/index.ts @@ -1,3 +1,4 @@ +export { default as Avatar } from './avatar.vue' export { default as Button } from './button.vue' export { default as Callout } from './callout.vue' export { default as ContainerError } from './container-error.vue'