admin.spec.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import { expect, test, type Page } from '@playwright/test';
  2. const chat = {
  3. chat_id: '-1001234567890',
  4. title: '产品运营测试群',
  5. username: 'product_ops_test',
  6. type: 'supergroup',
  7. accessible: true,
  8. member_count: 286,
  9. bot_status: 'administrator',
  10. bot_privileges: ['can_change_info', 'can_delete_messages', 'can_restrict_members'],
  11. };
  12. const recentMember = {
  13. user: {
  14. id: '10001',
  15. username: 'alice',
  16. first_name: '小明',
  17. last_name: '同学',
  18. is_bot: false,
  19. is_deleted: false,
  20. },
  21. status: 'recent',
  22. privileges: [],
  23. last_seen_at: '2026-07-24T08:00:00Z',
  24. };
  25. const bot = {
  26. bot_id: 'primary',
  27. label: 'Test Bot',
  28. enabled: true,
  29. bot_token_configured: true,
  30. sudo_users_id: ['10001'],
  31. log_group_id: '0',
  32. gban_log_group_id: '0',
  33. message_dump_chat: '-1001234567890',
  34. identity: { id: '999', username: 'test_bot', name: 'Test Bot' },
  35. ready_to_connect: true,
  36. runtime: { state: 'running' },
  37. };
  38. function envelope(data: unknown) {
  39. return JSON.stringify({ data });
  40. }
  41. async function selectChatWorkspace(page: Page, name: string) {
  42. if ((page.viewportSize()?.width || 0) <= 575) {
  43. await page.evaluate(() => window.scrollTo(0, 0));
  44. await page.locator('.workspace-tab-select').click();
  45. await page.getByText(name, { exact: true }).last().click();
  46. return;
  47. }
  48. await page.getByRole('tab', { name }).click();
  49. }
  50. test.beforeEach(async ({ page }) => {
  51. await page.route('**/api/admin/v1/**', async (route) => {
  52. const url = new URL(route.request().url());
  53. const path = url.pathname;
  54. let data: unknown = {};
  55. if (path.endsWith('/auth/login') || path.endsWith('/auth/me')) {
  56. data = { username: 'admin', must_change_password: false, csrf_token: 'csrf-test' };
  57. } else if (path.endsWith('/bots')) {
  58. data = { items: [bot] };
  59. } else if (path.endsWith('/settings/telegram')) {
  60. data = {
  61. api_id: 12345,
  62. api_hash_configured: true,
  63. api_ready: true,
  64. bots: [bot],
  65. };
  66. } else if (path.endsWith('/dashboard')) {
  67. data = {
  68. counts: { chats: 3, giveaways: 8, point_accounts: 126, point_transactions: 804, audit_events: 19 },
  69. recent_giveaways: [
  70. {
  71. giveaway_id: 'a1b2c3',
  72. chat_id: chat.chat_id,
  73. title: '七月活跃抽奖',
  74. status: 'running',
  75. prizes: [{ name: '一等奖', count: 1, points_reward: 50 }],
  76. minimum_points: 20,
  77. entry_cost: 5,
  78. participation_reward: 2,
  79. ends_at: '2026-07-25T08:00:00Z',
  80. created_at: '2026-07-24T08:00:00Z',
  81. },
  82. ],
  83. top_accounts: [{ chat_id: chat.chat_id, user_id: '10001', username: 'alice', display_name: '爱丽丝 示例', balance: 180, lifetime_earned: 220, lifetime_spent: 40 }],
  84. recent_transactions: [{ transaction_id: 'tx1', chat_id: chat.chat_id, user_id: '10001', username: 'alice', display_name: '爱丽丝 示例', delta: 10, balance_after: 180, source: 'checkin', created_at: '2026-07-24T08:00:00Z' }],
  85. };
  86. } else if (path.endsWith('/chats')) {
  87. data = { items: [chat], total: 1, page: 1, page_size: 20 };
  88. } else if (path === `/api/admin/v1/chats/${chat.chat_id}`) {
  89. data = {
  90. ...chat,
  91. description: '用于后台验收的测试群',
  92. permissions: {},
  93. automation: {
  94. chat_id: chat.chat_id,
  95. auto_replies: [],
  96. blacklist_words: ['广告'],
  97. risk_rules: [
  98. {
  99. rule_id: 'ads',
  100. name: '广告拦截',
  101. enabled: true,
  102. keywords: ['广告'],
  103. match_images: true,
  104. match_links: true,
  105. actions: ['delete', 'warn'],
  106. duration_seconds: 3600,
  107. },
  108. ],
  109. identity_monitor: { enabled: true, notify_in_chat: false },
  110. welcome: { enabled: false, text: '', media: null },
  111. captcha_enabled: false,
  112. chatbot_enabled: false,
  113. antiflood_enabled: true,
  114. },
  115. };
  116. } else if (path.endsWith('/admins')) {
  117. data = { items: [] };
  118. } else if (path.endsWith('/members/recent')) {
  119. data = { items: [recentMember] };
  120. } else if (path.endsWith('/members/identity-changes')) {
  121. data = {
  122. items: [{
  123. change_id: 'change-1',
  124. chat_id: chat.chat_id,
  125. user_id: '10001',
  126. before: { username: 'alice', display_name: '小明 同学' },
  127. after: { username: 'alice_new', display_name: '小明 新昵称' },
  128. changed_fields: ['username', 'display_name'],
  129. observed_at: '2026-07-24T08:30:00Z',
  130. }],
  131. total: 1,
  132. page: 1,
  133. page_size: 20,
  134. };
  135. } else if (path.endsWith('/members/search')) {
  136. data = { items: [{ ...recentMember, status: 'member' }] };
  137. } else if (path.endsWith('/giveaways')) {
  138. data = { items: [], total: 0, page: 1, page_size: 20 };
  139. } else if (path.endsWith('/points/accounts') || path.endsWith('/points/transactions')) {
  140. data = { items: [], total: 0, page: 1, page_size: 20 };
  141. } else if (path.endsWith('/audit-logs')) {
  142. data = { items: [], total: 0, page: 1, page_size: 20 };
  143. } else if (path.endsWith('/settings')) {
  144. data = {
  145. web_address: 'http://127.0.0.1:8088/admin',
  146. bind_host: '0.0.0.0',
  147. upload_limit_mb: 20,
  148. supervisor_mode: true,
  149. bot: { connected: false, id: '', username: '', name: '' },
  150. };
  151. }
  152. await route.fulfill({ status: 200, contentType: 'application/json', body: envelope(data) });
  153. });
  154. });
  155. test('登录与主要管理视图在不同视口无页面级横向滚动', async ({ page }, testInfo) => {
  156. await page.goto('/admin/login');
  157. await page.getByLabel('密码').fill('qwe0.123456');
  158. await page.getByRole('button', { name: /登\s*录/ }).click();
  159. await expect(page.getByRole('heading', { name: '概览' })).toBeVisible();
  160. const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
  161. expect(overflow).toBeLessThanOrEqual(1);
  162. await page.screenshot({ path: testInfo.outputPath('dashboard.png'), fullPage: true });
  163. await page.goto('/admin/chats');
  164. await expect(page.getByText('产品运营测试群')).toBeVisible();
  165. expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
  166. await page.goto(`/admin/chats/${chat.chat_id}`);
  167. await selectChatWorkspace(page, '自动化与风控');
  168. await expect(page.getByRole('heading', { name: '内容风控' })).toBeVisible();
  169. await expect(page.getByText('广告拦截')).toBeVisible();
  170. await page.getByRole('button', { name: '编辑 广告拦截' }).click();
  171. await expect(page.getByText('编辑风控规则')).toBeVisible();
  172. await expect(page.getByLabel('封禁时长')).toHaveValue('60');
  173. expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
  174. await page.getByRole('button', { name: '关闭' }).click();
  175. await selectChatWorkspace(page, '成员与管理员');
  176. await expect(page.getByText('小明 同学', { exact: true }).first()).toBeVisible();
  177. await expect(page.getByText('小明 新昵称', { exact: true }).first()).toBeVisible();
  178. expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
  179. await page.goto('/admin/giveaways');
  180. await expect(page.getByRole('heading', { name: '抽奖管理' })).toBeVisible();
  181. expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
  182. for (const [path, heading] of [
  183. ['/admin/points', '积分管理'],
  184. ['/admin/audit', '操作日志'],
  185. ['/admin/settings', '系统设置'],
  186. ] as const) {
  187. await page.goto(path);
  188. await expect(page.getByRole('heading', { name: heading })).toBeVisible();
  189. expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
  190. }
  191. });