Points.test.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. import { fireEvent, render, screen, waitFor } from '@testing-library/react';
  2. import { beforeEach, expect, it, vi } from 'vitest';
  3. import PointsPage from '@/pages/Points';
  4. import { apiRequest } from '@/services/api';
  5. vi.mock('@/services/api', () => ({
  6. apiRequest: vi.fn(),
  7. downloadFile: vi.fn(),
  8. jsonOptions: (method: string, body: unknown) => ({ method, body: JSON.stringify(body) }),
  9. }));
  10. const rules = {
  11. enabled: true,
  12. timezone: 'Asia/Shanghai',
  13. checkin_enabled: true,
  14. checkin_button_enabled: true,
  15. checkin_points: 10,
  16. activity_enabled: true,
  17. activity_points: 1,
  18. activity_cooldown_seconds: 300,
  19. activity_daily_cap: 10,
  20. upvote_enabled: true,
  21. upvote_points: 2,
  22. upvote_pair_cooldown_seconds: 86400,
  23. upvote_daily_cap: 10,
  24. };
  25. beforeEach(() => {
  26. vi.clearAllMocks();
  27. vi.mocked(apiRequest).mockImplementation(async (path) => {
  28. if (path.startsWith('/chats?')) {
  29. return {
  30. items: [
  31. { chat_id: '-100999', title: '不可用积分群', type: 'supergroup', accessible: false },
  32. { chat_id: '-100123', title: '积分测试群', type: 'supergroup', accessible: true },
  33. ],
  34. total: 2,
  35. page: 1,
  36. page_size: 100,
  37. };
  38. }
  39. if (path.startsWith('/points/accounts?')) {
  40. return {
  41. items: [{ chat_id: '-100123', user_id: '10001', username: 'alice', display_name: '爱丽丝 示例', balance: 180, lifetime_earned: 220, lifetime_spent: 40 }],
  42. total: 1,
  43. page: 1,
  44. page_size: 20,
  45. };
  46. }
  47. if (path.startsWith('/points/transactions?')) {
  48. return {
  49. 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' }],
  50. total: 1,
  51. page: 1,
  52. page_size: 20,
  53. };
  54. }
  55. if (path.endsWith('/points/settings')) return rules;
  56. if (path === '/points/adjustments') return { created: true };
  57. return {};
  58. });
  59. });
  60. it('展示余额排行、流水并保存积分规则', async () => {
  61. render(<PointsPage />);
  62. expect(await screen.findByText('爱丽丝 示例')).toBeInTheDocument();
  63. expect(screen.queryByText('@alice')).not.toBeInTheDocument();
  64. expect(
  65. vi.mocked(apiRequest).mock.calls.some(([path]) => path.includes('chat_id=-100999')),
  66. ).toBe(false);
  67. fireEvent.click(screen.getByRole('tab', { name: /流水/ }));
  68. expect(await screen.findByText('Daily check-in')).toBeInTheDocument();
  69. fireEvent.click(screen.getByRole('tab', { name: '自动积分规则' }));
  70. expect(await screen.findByRole('heading', { name: '签到' })).toBeInTheDocument();
  71. expect(screen.getByRole('switch', { name: '底部签到按钮' })).toBeChecked();
  72. await waitFor(() =>
  73. expect(apiRequest).toHaveBeenCalledWith('/chats/-100123/points/settings'),
  74. );
  75. const saveButton = screen.getByRole('button', { name: /保\s*存\s*规\s*则/ });
  76. await waitFor(() => expect(saveButton).toBeEnabled());
  77. fireEvent.click(saveButton);
  78. expect(
  79. vi.mocked(apiRequest).mock.calls.some(
  80. ([path, options]) => path === '/chats/-100123/points/settings' && options?.method === 'PUT',
  81. ),
  82. ).toBe(false);
  83. fireEvent.click(await screen.findByRole('button', { name: /确\s*认\s*保\s*存/ }));
  84. await waitFor(() => {
  85. const call = vi.mocked(apiRequest).mock.calls.find(
  86. ([path, options]) => path === '/chats/-100123/points/settings' && options?.method === 'PUT',
  87. );
  88. expect(call).toBeTruthy();
  89. expect(JSON.parse(String(call?.[1]?.body))).toMatchObject({
  90. checkin_button_enabled: true,
  91. confirm: true,
  92. });
  93. });
  94. });
  95. it('手工调整必须二次确认并携带确认标记', async () => {
  96. render(<PointsPage />);
  97. await screen.findByText('爱丽丝 示例');
  98. fireEvent.click(screen.getByRole('button', { name: /手\s*工\s*调\s*整/ }));
  99. fireEvent.change(screen.getByLabelText('用户 ID'), { target: { value: '10001' } });
  100. fireEvent.change(screen.getByLabelText('积分数值'), { target: { value: '25' } });
  101. fireEvent.change(screen.getByLabelText('原因'), { target: { value: '活动补发' } });
  102. fireEvent.click(screen.getByRole('button', { name: /继\s*续\s*确\s*认/ }));
  103. expect(vi.mocked(apiRequest).mock.calls.some(([path]) => path === '/points/adjustments')).toBe(false);
  104. fireEvent.click(await screen.findByRole('button', { name: /确\s*认\s*写\s*入/ }));
  105. await waitFor(() => {
  106. const call = vi.mocked(apiRequest).mock.calls.find(([path]) => path === '/points/adjustments');
  107. expect(call).toBeTruthy();
  108. expect(JSON.parse(String(call?.[1]?.body))).toMatchObject({
  109. chat_id: '-100123',
  110. user_id: '10001',
  111. amount: 25,
  112. reason: '活动补发',
  113. confirm: true,
  114. });
  115. });
  116. });