admin.spec.ts 10 KB

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