diff --git a/src/components/topbar/CurrentUserButton.spec.ts b/src/components/topbar/CurrentUserButton.spec.ts new file mode 100644 index 000000000..abb2a08b7 --- /dev/null +++ b/src/components/topbar/CurrentUserButton.spec.ts @@ -0,0 +1,122 @@ +import { VueWrapper, mount } from '@vue/test-utils' +import Button from 'primevue/button' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { h } from 'vue' +import { createI18n } from 'vue-i18n' + +import enMessages from '@/locales/en/main.json' + +import CurrentUserButton from './CurrentUserButton.vue' + +// Mock all firebase modules +vi.mock('firebase/app', () => ({ + initializeApp: vi.fn(), + getApp: vi.fn() +})) + +vi.mock('firebase/auth', () => ({ + getAuth: vi.fn(), + setPersistence: vi.fn(), + browserLocalPersistence: {}, + onAuthStateChanged: vi.fn(), + signInWithEmailAndPassword: vi.fn(), + signOut: vi.fn() +})) + +// Mock pinia +vi.mock('pinia') + +// Mock the useCurrentUser composable +vi.mock('@/composables/auth/useCurrentUser', () => ({ + useCurrentUser: vi.fn(() => ({ + isLoggedIn: true, + userPhotoUrl: 'https://example.com/avatar.jpg', + userDisplayName: 'Test User', + userEmail: 'test@example.com' + })) +})) + +// Mock the UserAvatar component +vi.mock('@/components/common/UserAvatar.vue', () => ({ + default: { + name: 'UserAvatarMock', + render() { + return h('div', 'Avatar') + } + } +})) + +// Mock the CurrentUserPopover component +vi.mock('./CurrentUserPopover.vue', () => ({ + default: { + name: 'CurrentUserPopoverMock', + render() { + return h('div', 'Popover Content') + }, + emits: ['close'] + } +})) + +describe('CurrentUserButton', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + const mountComponent = (): VueWrapper => { + const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { en: enMessages } + }) + + return mount(CurrentUserButton, { + global: { + plugins: [i18n], + stubs: { + // Use shallow mount for popover to make testing easier + Popover: { + template: '
', + methods: { + toggle: vi.fn(), + hide: vi.fn() + } + }, + Button: true + } + } + }) + } + + it('renders correctly when user is logged in', () => { + const wrapper = mountComponent() + expect(wrapper.findComponent(Button).exists()).toBe(true) + }) + + it('toggles popover on button click', async () => { + const wrapper = mountComponent() + const popoverToggleSpy = vi.fn() + + // Override the ref with a mock implementation + // @ts-expect-error - accessing internal Vue component vm + wrapper.vm.popover = { toggle: popoverToggleSpy } + + await wrapper.findComponent(Button).trigger('click') + expect(popoverToggleSpy).toHaveBeenCalled() + }) + + it('hides popover when closePopover is called', async () => { + const wrapper = mountComponent() + + // Replace the popover.hide method with a spy + const popoverHideSpy = vi.fn() + // @ts-expect-error - accessing internal Vue component vm + wrapper.vm.popover = { hide: popoverHideSpy } + + // Directly call the closePopover method through the component instance + // @ts-expect-error - accessing internal Vue component vm + wrapper.vm.closePopover() + + // Verify that popover.hide was called + expect(popoverHideSpy).toHaveBeenCalled() + }) +}) diff --git a/src/components/topbar/CurrentUserButton.vue b/src/components/topbar/CurrentUserButton.vue index a7644143e..c5d8809d4 100644 --- a/src/components/topbar/CurrentUserButton.vue +++ b/src/components/topbar/CurrentUserButton.vue @@ -19,7 +19,7 @@ - + @@ -40,4 +40,8 @@ const popover = ref | null>(null) const photoURL = computed( () => userPhotoUrl.value ?? undefined ) + +const closePopover = () => { + popover.value?.hide() +} diff --git a/src/components/topbar/CurrentUserPopover.spec.ts b/src/components/topbar/CurrentUserPopover.spec.ts new file mode 100644 index 000000000..954359021 --- /dev/null +++ b/src/components/topbar/CurrentUserPopover.spec.ts @@ -0,0 +1,173 @@ +import { VueWrapper, mount } from '@vue/test-utils' +import Button from 'primevue/button' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { h } from 'vue' +import { createI18n } from 'vue-i18n' + +import enMessages from '@/locales/en/main.json' + +import CurrentUserPopover from './CurrentUserPopover.vue' + +// Mock all firebase modules +vi.mock('firebase/app', () => ({ + initializeApp: vi.fn(), + getApp: vi.fn() +})) + +vi.mock('firebase/auth', () => ({ + getAuth: vi.fn(), + setPersistence: vi.fn(), + browserLocalPersistence: {}, + onAuthStateChanged: vi.fn(), + signInWithEmailAndPassword: vi.fn(), + signOut: vi.fn() +})) + +// Mock pinia +vi.mock('pinia') + +// Mock showSettingsDialog and showTopUpCreditsDialog +const mockShowSettingsDialog = vi.fn() +const mockShowTopUpCreditsDialog = vi.fn() + +// Mock window.open +const originalWindowOpen = window.open +beforeEach(() => { + window.open = vi.fn() +}) + +afterAll(() => { + window.open = originalWindowOpen +}) + +// Mock the useCurrentUser composable +vi.mock('@/composables/auth/useCurrentUser', () => ({ + useCurrentUser: vi.fn(() => ({ + userPhotoUrl: 'https://example.com/avatar.jpg', + userDisplayName: 'Test User', + userEmail: 'test@example.com' + })) +})) + +// Mock the useFirebaseAuthActions composable +vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({ + useFirebaseAuthActions: vi.fn(() => ({ + fetchBalance: vi.fn().mockResolvedValue(undefined) + })) +})) + +// Mock the dialog service +vi.mock('@/services/dialogService', () => ({ + useDialogService: vi.fn(() => ({ + showSettingsDialog: mockShowSettingsDialog, + showTopUpCreditsDialog: mockShowTopUpCreditsDialog + })) +})) + +// Mock UserAvatar component +vi.mock('@/components/common/UserAvatar.vue', () => ({ + default: { + name: 'UserAvatarMock', + render() { + return h('div', 'Avatar') + } + } +})) + +// Mock UserCredit component +vi.mock('@/components/common/UserCredit.vue', () => ({ + default: { + name: 'UserCreditMock', + render() { + return h('div', 'Credit: 100') + } + } +})) + +describe('CurrentUserPopover', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + const mountComponent = (): VueWrapper => { + const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { en: enMessages } + }) + + return mount(CurrentUserPopover, { + global: { + plugins: [i18n], + stubs: { + Divider: true, + Button: true + } + } + }) + } + + it('renders user information correctly', () => { + const wrapper = mountComponent() + + expect(wrapper.text()).toContain('Test User') + expect(wrapper.text()).toContain('test@example.com') + }) + + it('opens user settings and emits close event when settings button is clicked', async () => { + const wrapper = mountComponent() + + // Find all buttons and get the settings button (first one) + const buttons = wrapper.findAllComponents(Button) + const settingsButton = buttons[0] + + // Click the settings button + await settingsButton.trigger('click') + + // Verify showSettingsDialog was called with 'user' + expect(mockShowSettingsDialog).toHaveBeenCalledWith('user') + + // Verify close event was emitted + expect(wrapper.emitted('close')).toBeTruthy() + expect(wrapper.emitted('close')!.length).toBe(1) + }) + + it('opens API pricing docs and emits close event when API pricing button is clicked', async () => { + const wrapper = mountComponent() + + // Find all buttons and get the API pricing button (second one) + const buttons = wrapper.findAllComponents(Button) + const apiPricingButton = buttons[1] + + // Click the API pricing button + await apiPricingButton.trigger('click') + + // Verify window.open was called with the correct URL + expect(window.open).toHaveBeenCalledWith( + 'https://docs.comfy.org/tutorials/api-nodes/pricing', + '_blank' + ) + + // Verify close event was emitted + expect(wrapper.emitted('close')).toBeTruthy() + expect(wrapper.emitted('close')!.length).toBe(1) + }) + + it('opens top-up dialog and emits close event when top-up button is clicked', async () => { + const wrapper = mountComponent() + + // Find all buttons and get the top-up button (last one) + const buttons = wrapper.findAllComponents(Button) + const topUpButton = buttons[buttons.length - 1] + + // Click the top-up button + await topUpButton.trigger('click') + + // Verify showTopUpCreditsDialog was called + expect(mockShowTopUpCreditsDialog).toHaveBeenCalled() + + // Verify close event was emitted + expect(wrapper.emitted('close')).toBeTruthy() + expect(wrapper.emitted('close')!.length).toBe(1) + }) +}) diff --git a/src/components/topbar/CurrentUserPopover.vue b/src/components/topbar/CurrentUserPopover.vue index 8f8751526..52e364d95 100644 --- a/src/components/topbar/CurrentUserPopover.vue +++ b/src/components/topbar/CurrentUserPopover.vue @@ -72,20 +72,27 @@ import { useCurrentUser } from '@/composables/auth/useCurrentUser' import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions' import { useDialogService } from '@/services/dialogService' +const emit = defineEmits<{ + close: [] +}>() + const { userDisplayName, userEmail, userPhotoUrl } = useCurrentUser() const authActions = useFirebaseAuthActions() const dialogService = useDialogService() const handleOpenUserSettings = () => { dialogService.showSettingsDialog('user') + emit('close') } const handleTopUp = () => { dialogService.showTopUpCreditsDialog() + emit('close') } const handleOpenApiPricing = () => { window.open('https://docs.comfy.org/tutorials/api-nodes/pricing', '_blank') + emit('close') } onMounted(() => {