113 lines
5.2 KiB
TypeScript
113 lines
5.2 KiB
TypeScript
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<void> }) => 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(<MemoryRouter initialEntries={['/coding']}><Sidebar /></MemoryRouter>);
|
||
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 点');
|
||
expect(screen.getByTestId('point-recharge-recipient')).toHaveTextContent('测试用户(member@example.com)');
|
||
});
|
||
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 点'));
|
||
});
|
||
});
|