mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +00:00
Fix: Close user popover on button clicks (#3928)
This commit is contained in:
122
src/components/topbar/CurrentUserButton.spec.ts
Normal file
122
src/components/topbar/CurrentUserButton.spec.ts
Normal file
@@ -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: '<div><slot></slot></div>',
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Popover ref="popover" :show-arrow="false">
|
<Popover ref="popover" :show-arrow="false">
|
||||||
<CurrentUserPopover />
|
<CurrentUserPopover @close="closePopover" />
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -40,4 +40,8 @@ const popover = ref<InstanceType<typeof Popover> | null>(null)
|
|||||||
const photoURL = computed<string | undefined>(
|
const photoURL = computed<string | undefined>(
|
||||||
() => userPhotoUrl.value ?? undefined
|
() => userPhotoUrl.value ?? undefined
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const closePopover = () => {
|
||||||
|
popover.value?.hide()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
173
src/components/topbar/CurrentUserPopover.spec.ts
Normal file
173
src/components/topbar/CurrentUserPopover.spec.ts
Normal file
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -72,20 +72,27 @@ import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
|||||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||||
import { useDialogService } from '@/services/dialogService'
|
import { useDialogService } from '@/services/dialogService'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: []
|
||||||
|
}>()
|
||||||
|
|
||||||
const { userDisplayName, userEmail, userPhotoUrl } = useCurrentUser()
|
const { userDisplayName, userEmail, userPhotoUrl } = useCurrentUser()
|
||||||
const authActions = useFirebaseAuthActions()
|
const authActions = useFirebaseAuthActions()
|
||||||
const dialogService = useDialogService()
|
const dialogService = useDialogService()
|
||||||
|
|
||||||
const handleOpenUserSettings = () => {
|
const handleOpenUserSettings = () => {
|
||||||
dialogService.showSettingsDialog('user')
|
dialogService.showSettingsDialog('user')
|
||||||
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTopUp = () => {
|
const handleTopUp = () => {
|
||||||
dialogService.showTopUpCreditsDialog()
|
dialogService.showTopUpCreditsDialog()
|
||||||
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOpenApiPricing = () => {
|
const handleOpenApiPricing = () => {
|
||||||
window.open('https://docs.comfy.org/tutorials/api-nodes/pricing', '_blank')
|
window.open('https://docs.comfy.org/tutorials/api-nodes/pricing', '_blank')
|
||||||
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user