112 lines
5.1 KiB
TypeScript
112 lines
5.1 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 点');
|
|
});
|
|
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 点'));
|
|
});
|
|
});
|