import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { beforeEach, expect, it, vi } from 'vitest';
import PointsPage from '@/pages/Points';
import { apiRequest } from '@/services/api';
vi.mock('@/services/api', () => ({
apiRequest: vi.fn(),
downloadFile: vi.fn(),
jsonOptions: (method: string, body: unknown) => ({ method, body: JSON.stringify(body) }),
}));
const rules = {
enabled: true,
timezone: 'Asia/Shanghai',
checkin_enabled: true,
checkin_button_enabled: true,
checkin_points: 10,
activity_enabled: true,
activity_points: 1,
activity_cooldown_seconds: 300,
activity_daily_cap: 10,
upvote_enabled: true,
upvote_points: 2,
upvote_pair_cooldown_seconds: 86400,
upvote_daily_cap: 10,
};
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(apiRequest).mockImplementation(async (path) => {
if (path.startsWith('/chats?')) {
return {
items: [{ chat_id: '-100123', title: '积分测试群', type: 'supergroup', accessible: true }],
total: 1,
page: 1,
page_size: 100,
};
}
if (path.startsWith('/points/accounts?')) {
return {
items: [{ chat_id: '-100123', user_id: '10001', username: 'alice', display_name: '爱丽丝 示例', balance: 180, lifetime_earned: 220, lifetime_spent: 40 }],
total: 1,
page: 1,
page_size: 20,
};
}
if (path.startsWith('/points/transactions?')) {
return {
items: [{ transaction_id: 'tx-1', chat_id: '-100123', user_id: '10001', username: 'alice', display_name: '爱丽丝 示例', delta: 10, balance_after: 180, source: 'checkin', reason: 'Daily check-in', created_at: '2026-07-24T08:00:00Z' }],
total: 1,
page: 1,
page_size: 20,
};
}
if (path.endsWith('/points/settings')) return rules;
if (path === '/points/adjustments') return { created: true };
return {};
});
});
it('展示余额排行、流水并保存积分规则', async () => {
render();
expect(await screen.findByText('爱丽丝 示例')).toBeInTheDocument();
expect(screen.queryByText('@alice')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('tab', { name: /流水/ }));
expect(await screen.findByText('Daily check-in')).toBeInTheDocument();
fireEvent.click(screen.getByRole('tab', { name: '自动积分规则' }));
expect(await screen.findByRole('heading', { name: '签到' })).toBeInTheDocument();
expect(screen.getByRole('switch', { name: '底部签到按钮' })).toBeChecked();
await waitFor(() =>
expect(apiRequest).toHaveBeenCalledWith('/chats/-100123/points/settings'),
);
const saveButton = screen.getByRole('button', { name: /保\s*存\s*规\s*则/ });
await waitFor(() => expect(saveButton).toBeEnabled());
fireEvent.click(saveButton);
expect(
vi.mocked(apiRequest).mock.calls.some(
([path, options]) => path === '/chats/-100123/points/settings' && options?.method === 'PUT',
),
).toBe(false);
fireEvent.click(await screen.findByRole('button', { name: /确\s*认\s*保\s*存/ }));
await waitFor(() => {
const call = vi.mocked(apiRequest).mock.calls.find(
([path, options]) => path === '/chats/-100123/points/settings' && options?.method === 'PUT',
);
expect(call).toBeTruthy();
expect(JSON.parse(String(call?.[1]?.body))).toMatchObject({
checkin_button_enabled: true,
confirm: true,
});
});
});
it('手工调整必须二次确认并携带确认标记', async () => {
render();
await screen.findByText('爱丽丝 示例');
fireEvent.click(screen.getByRole('button', { name: /手\s*工\s*调\s*整/ }));
fireEvent.change(screen.getByLabelText('用户 ID'), { target: { value: '10001' } });
fireEvent.change(screen.getByLabelText('积分数值'), { target: { value: '25' } });
fireEvent.change(screen.getByLabelText('原因'), { target: { value: '活动补发' } });
fireEvent.click(screen.getByRole('button', { name: /继\s*续\s*确\s*认/ }));
expect(vi.mocked(apiRequest).mock.calls.some(([path]) => path === '/points/adjustments')).toBe(false);
fireEvent.click(await screen.findByRole('button', { name: /确\s*认\s*写\s*入/ }));
await waitFor(() => {
const call = vi.mocked(apiRequest).mock.calls.find(([path]) => path === '/points/adjustments');
expect(call).toBeTruthy();
expect(JSON.parse(String(call?.[1]?.body))).toMatchObject({
chat_id: '-100123',
user_id: '10001',
amount: 25,
reason: '活动补发',
confirm: true,
});
});
});