import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Sidebar } from '@/components/layout/Sidebar'; import { billingSummary, permanentBalance } from '../fixtures/permanent-points'; const fetchBillingSummaryMock = vi.hoisted(() => vi.fn()); const getValidAccessTokenMock = vi.hoisted(() => vi.fn()); const authState = vi.hoisted(() => ({ user: { username: 'member@example.com', userId: 'user-1', tenantId: null, deptId: null, authorities: [], }, getValidAccessToken: getValidAccessTokenMock, logout: vi.fn(), })); const codingState = vi.hoisted(() => ({ projects: [], activeProject: null, load: vi.fn().mockResolvedValue(undefined), setActiveProject: vi.fn(), removeProject: vi.fn(), createProject: vi.fn(), })); const projectConfigState = vi.hoisted(() => ({ configsByProjectId: {}, load: vi.fn().mockResolvedValue(undefined), remove: vi.fn(), })); vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (_key: string, fallback: string) => fallback }), })); vi.mock('@/lib/works-billing', () => ({ fetchWorksBillingSummary: (...args: unknown[]) => fetchBillingSummaryMock(...args), fetchWorksRechargeOrders: vi.fn().mockResolvedValue({ items: [] }), fetchWorksPointHistory: vi.fn().mockResolvedValue({ items: [], has_more: false }), createWorksRechargeOrder: vi.fn(), fetchWorksRechargeOrder: vi.fn(), openWorksBillingAccount: vi.fn(), })); vi.mock('@/lib/api-client', () => ({ invokeIpc: vi.fn() })); vi.mock('@/hooks/use-current-user-profile', () => ({ useCurrentUserProfile: () => ({ userProfile: { displayName: '测试用户', avatarUrl: null }, profileRequired: false, profileSyncError: null, syncProfileNow: vi.fn().mockResolvedValue(undefined), }), })); vi.mock('@/stores/auth', () => ({ useAuthStore: (selector: (state: typeof authState) => unknown) => selector(authState), })); vi.mock('@/stores/settings', () => ({ useSettingsStore: (selector: (state: { sidebarCollapsed: boolean }) => unknown) => ( selector({ sidebarCollapsed: false }) ), })); vi.mock('@/stores/coding-workspace', () => ({ useCodingWorkspaceStore: (selector: (state: typeof codingState) => unknown) => selector(codingState), })); vi.mock('@/stores/project-config', () => ({ useProjectConfigStore: (selector: (state: typeof projectConfigState) => unknown) => ( selector(projectConfigState) ), })); vi.mock('@/stores/providers', () => ({ useProviderStore: (selector: (state: { refreshProviderSnapshot: () => Promise }) => unknown) => ( selector({ refreshProviderSnapshot: vi.fn().mockResolvedValue(undefined) }) ), })); vi.mock('@/components/layout/ModuleSwitcher', () => ({ ModuleSwitcher: () => null })); vi.mock('@/components/layout/ImageWorkspaceSidebar', () => ({ ImageWorkspaceSidebar: () => null })); vi.mock('@/components/layout/SidebarUpdateButton', () => ({ SidebarUpdateButton: () => null })); vi.mock('@/components/profile/UserProfileDialog', () => ({ UserProfileDialog: () => null })); async function openAccount() { render(); fireEvent.click(screen.getByTestId('sidebar-member-menu-trigger')); } describe('Sidebar permanent point wallet', () => { beforeEach(() => { fetchBillingSummaryMock.mockReset(); fetchBillingSummaryMock.mockResolvedValue(billingSummary()); }); it('opens the point wallet from the account menu without retired member controls', async () => { await openAccount(); await waitFor(() => expect(screen.getByTestId('sidebar-total-token-points')).toHaveTextContent('75 点')); expect(screen.queryByText(/重置卡|升级订阅|会员等级|本周剩余/)).not.toBeInTheDocument(); fireEvent.click(screen.getByTestId('sidebar-point-wallet-open')); expect(screen.getByTestId('point-wallet-dialog')).toHaveTextContent('1 元充值 50 点'); }); it('shows a retryable failure without inventing a zero balance', async () => { fetchBillingSummaryMock.mockRejectedValue(new Error('offline')); await openAccount(); await waitFor(() => expect(screen.getByTestId('sidebar-total-token-points')).toHaveTextContent('暂时不可用')); fetchBillingSummaryMock.mockResolvedValue(billingSummary()); fireEvent.click(screen.getByRole('button', { name: '重试余额' })); await waitFor(() => expect(screen.getByTestId('sidebar-total-token-points')).toHaveTextContent('75 点')); }); it('refreshes the balance when reopening the account menu after consumption', async () => { await openAccount(); await waitFor(() => expect(screen.getByTestId('sidebar-total-token-points')).toHaveTextContent('75 点')); fireEvent.click(screen.getByTestId('sidebar-member-menu-trigger')); fetchBillingSummaryMock.mockResolvedValue(billingSummary({ own_points: permanentBalance({ total_remaining: '50.00' }) })); fireEvent.click(screen.getByTestId('sidebar-member-menu-trigger')); await waitFor(() => expect(screen.getByTestId('sidebar-total-token-points')).toHaveTextContent('50 点')); }); });