117 lines
5.5 KiB
TypeScript
117 lines
5.5 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('keeps the balance and account controls while hiding the recharge entry', async () => {
|
|
await openAccount();
|
|
await waitFor(() => expect(screen.getByTestId('sidebar-total-token-points')).toHaveTextContent('75 点'));
|
|
expect(screen.queryByText(/重置卡|升级订阅|会员等级|本周剩余/)).not.toBeInTheDocument();
|
|
expect(screen.getByTestId('sidebar-point-balance')).toHaveTextContent('永久有效 · 预占 5 点');
|
|
expect(screen.queryByTestId('sidebar-point-wallet-open')).not.toBeInTheDocument();
|
|
expect(screen.queryByRole('menuitem', { name: '词元点数与充值' })).not.toBeInTheDocument();
|
|
expect(screen.queryByTestId('point-wallet-dialog')).not.toBeInTheDocument();
|
|
expect(screen.getByRole('menuitem', { name: '个人资料' })).toBeVisible();
|
|
expect(screen.getByRole('menuitem', { name: '设置' })).toBeVisible();
|
|
expect(screen.getByRole('menuitem', { name: '退出登录' })).toBeVisible();
|
|
});
|
|
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 点'));
|
|
});
|
|
});
|