From c13343b8fb9cefadef1c050de6080fe9d720edc6 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Tue, 9 Dec 2025 19:47:57 -0800 Subject: [PATCH] style: redesign user popover with improved layout and integration with design system (#7303) Implements new Figma design for the user popover with cleaner row-based layout, proper design system tokens, and improved spacing. Replaces PrimeVue icons with Lucide icons, fixes credits display to show whole numbers without unnecessary decimals, updates menu item order to match design specifications, and ensures consistent hover states and typography throughout. All styling now uses Tailwind classes with proper semantic design tokens instead of inline styles. | Before | After | | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | image | image | --------- Co-authored-by: Alexander Brown --- .../topbar/CurrentUserPopover.test.ts | 103 +++++++--- src/components/topbar/CurrentUserPopover.vue | 191 ++++++++++-------- src/locales/en/main.json | 4 +- 3 files changed, 183 insertions(+), 115 deletions(-) diff --git a/src/components/topbar/CurrentUserPopover.test.ts b/src/components/topbar/CurrentUserPopover.test.ts index 1ad2321da..48477b934 100644 --- a/src/components/topbar/CurrentUserPopover.test.ts +++ b/src/components/topbar/CurrentUserPopover.test.ts @@ -1,10 +1,10 @@ import type { VueWrapper } from '@vue/test-utils' import { 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 { formatCreditsFromCents } from '@/base/credits/comfyCredits' import enMessages from '@/locales/en/main.json' with { type: 'json' } import CurrentUserPopover from './CurrentUserPopover.vue' @@ -74,7 +74,9 @@ vi.mock('@/stores/firebaseAuthStore', () => ({ useFirebaseAuthStore: vi.fn(() => ({ getAuthHeader: vi .fn() - .mockResolvedValue({ Authorization: 'Bearer mock-token' }) + .mockResolvedValue({ Authorization: 'Bearer mock-token' }), + balance: { amount_micros: 100_000 }, // 100,000 cents = ~211,000 credits + isFetchingBalance: false })) })) @@ -107,6 +109,39 @@ vi.mock('@/components/common/UserCredit.vue', () => ({ } })) +// Mock formatCreditsFromCents +vi.mock('@/base/credits/comfyCredits', () => ({ + formatCreditsFromCents: vi.fn(({ cents }) => (cents / 100).toString()) +})) + +// Mock useExternalLink +vi.mock('@/composables/useExternalLink', () => ({ + useExternalLink: vi.fn(() => ({ + buildDocsUrl: vi.fn((path) => `https://docs.comfy.org${path}`) + })) +})) + +// Mock useFeatureFlags +vi.mock('@/composables/useFeatureFlags', () => ({ + useFeatureFlags: vi.fn(() => ({ + flags: { + subscriptionTiersEnabled: true + } + })) +})) + +// Mock useTelemetry +vi.mock('@/platform/telemetry', () => ({ + useTelemetry: vi.fn(() => ({ + trackAddApiCreditButtonClicked: vi.fn() + })) +})) + +// Mock isCloud +vi.mock('@/platform/distribution/types', () => ({ + isCloud: true +})) + vi.mock('@/platform/cloud/subscription/components/SubscribeButton.vue', () => ({ default: { name: 'SubscribeButtonMock', @@ -145,27 +180,37 @@ describe('CurrentUserPopover', () => { expect(wrapper.text()).toContain('test@example.com') }) - it('renders logout button with correct props', () => { + it('calls formatCreditsFromCents with correct parameters and displays formatted credits', () => { const wrapper = mountComponent() - // Find all buttons and get the logout button (last button) - const buttons = wrapper.findAllComponents(Button) - const logoutButton = buttons[4] + expect(formatCreditsFromCents).toHaveBeenCalledWith({ + cents: 100_000, + locale: 'en', + numberOptions: { + minimumFractionDigits: 0, + maximumFractionDigits: 2 + } + }) - // Check that logout button has correct props - expect(logoutButton.props('label')).toBe('Log Out') - expect(logoutButton.props('icon')).toBe('pi pi-sign-out') + // Verify the formatted credit string (1000) is rendered in the DOM + expect(wrapper.text()).toContain('1000') }) - it('opens user settings and emits close event when settings button is clicked', async () => { + it('renders logout menu item with correct text', () => { const wrapper = mountComponent() - // Find all buttons and get the settings button (third button) - const buttons = wrapper.findAllComponents(Button) - const settingsButton = buttons[2] + const logoutItem = wrapper.find('[data-testid="logout-menu-item"]') + expect(logoutItem.exists()).toBe(true) + expect(wrapper.text()).toContain('Log Out') + }) - // Click the settings button - await settingsButton.trigger('click') + it('opens user settings and emits close event when settings item is clicked', async () => { + const wrapper = mountComponent() + + const settingsItem = wrapper.find('[data-testid="user-settings-menu-item"]') + expect(settingsItem.exists()).toBe(true) + + await settingsItem.trigger('click') // Verify showSettingsDialog was called with 'user' expect(mockShowSettingsDialog).toHaveBeenCalledWith('user') @@ -175,15 +220,13 @@ describe('CurrentUserPopover', () => { expect(wrapper.emitted('close')!.length).toBe(1) }) - it('calls logout function and emits close event when logout button is clicked', async () => { + it('calls logout function and emits close event when logout item is clicked', async () => { const wrapper = mountComponent() - // Find all buttons and get the logout button (last button) - const buttons = wrapper.findAllComponents(Button) - const logoutButton = buttons[4] + const logoutItem = wrapper.find('[data-testid="logout-menu-item"]') + expect(logoutItem.exists()).toBe(true) - // Click the logout button - await logoutButton.trigger('click') + await logoutItem.trigger('click') // Verify handleSignOut was called expect(mockHandleSignOut).toHaveBeenCalled() @@ -193,15 +236,15 @@ describe('CurrentUserPopover', () => { expect(wrapper.emitted('close')!.length).toBe(1) }) - it('opens API pricing docs and emits close event when API pricing button is clicked', async () => { + it('opens API pricing docs and emits close event when partner nodes item is clicked', async () => { const wrapper = mountComponent() - // Find all buttons and get the Partner Nodes info button (first one) - const buttons = wrapper.findAllComponents(Button) - const partnerNodesButton = buttons[0] + const partnerNodesItem = wrapper.find( + '[data-testid="partner-nodes-menu-item"]' + ) + expect(partnerNodesItem.exists()).toBe(true) - // Click the Partner Nodes button - await partnerNodesButton.trigger('click') + await partnerNodesItem.trigger('click') // Verify window.open was called with the correct URL expect(window.open).toHaveBeenCalledWith( @@ -217,11 +260,9 @@ describe('CurrentUserPopover', () => { 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 (second one) - const buttons = wrapper.findAllComponents(Button) - const topUpButton = buttons[1] + const topUpButton = wrapper.find('[data-testid="add-credits-button"]') + expect(topUpButton.exists()).toBe(true) - // Click the top-up button await topUpButton.trigger('click') // Verify showTopUpCreditsDialog was called diff --git a/src/components/topbar/CurrentUserPopover.vue b/src/components/topbar/CurrentUserPopover.vue index 1c964960f..144d3731f 100644 --- a/src/components/topbar/CurrentUserPopover.vue +++ b/src/components/topbar/CurrentUserPopover.vue @@ -1,120 +1,131 @@