admin.spec.ts 8.8 KB

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