import { expect, test, type Page } from '@playwright/test'; const chat = { chat_id: '-1001234567890', title: '产品运营测试群', username: 'product_ops_test', type: 'supergroup', accessible: true, member_count: 286, bot_status: 'administrator', bot_privileges: ['can_change_info', 'can_delete_messages', 'can_restrict_members'], }; const recentMember = { user: { id: '10001', username: 'alice', first_name: '小明', last_name: '同学', is_bot: false, is_deleted: false, }, status: 'recent', privileges: [], last_seen_at: '2026-07-24T08:00:00Z', }; const permissions = [ 'chat.profile', 'chat.members', 'chat.permissions', 'chat.announcements', 'chat.invites', 'chat.rules', 'chat.notes', 'automation.manage', 'points.manage', 'giveaways.manage', 'karma.manage', 'teacher_directory.manage', 'business_assistant.manage', 'media.upload', ]; const bot = { bot_id: 'primary', label: 'Test Bot', enabled: true, bot_token_configured: true, sudo_users_id: ['10001'], log_group_id: '0', gban_log_group_id: '0', message_dump_chat: '-1001234567890', role_ids: ['full_access'], permissions, identity: { id: '999', username: 'test_bot', name: 'Test Bot', can_connect_to_business: true }, business_assistant: { base_url: 'https://models.example.com/v1', api_key_configured: true, model: 'support-model', timeout_seconds: 30, max_output_tokens: 600, configured: true, }, ready_to_connect: true, runtime: { state: 'running' }, }; function envelope(data: unknown) { return JSON.stringify({ data }); } async function selectChatWorkspace(page: Page, name: string) { if ((page.viewportSize()?.width || 0) <= 575) { await page.evaluate(() => window.scrollTo(0, 0)); await page.locator('.workspace-tab-select').click(); await page.getByText(name, { exact: true }).last().click(); return; } await page.getByRole('tab', { name }).click(); } test.beforeEach(async ({ page }) => { await page.route('**/api/admin/v1/**', async (route) => { const url = new URL(route.request().url()); const path = url.pathname; let data: unknown = {}; if (path.endsWith('/auth/login') || path.endsWith('/auth/me')) { data = { username: 'admin', must_change_password: false, csrf_token: 'csrf-test' }; } else if (path.endsWith('/bots')) { data = { items: [bot] }; } else if (path.endsWith('/settings/telegram')) { data = { api_id: 12345, api_hash_configured: true, api_ready: true, permission_catalog: permissions.map((key) => ({ key, name: key, group: '职责权限', description: key, })), roles: [ { role_id: 'full_access', name: '全部职责', description: '拥有全部机器人职责', permissions, builtin: true, }, ], bots: [bot], }; } else if (path.endsWith('/dashboard')) { data = { counts: { chats: 3, giveaways: 8, point_accounts: 126, point_transactions: 804, audit_events: 19 }, recent_giveaways: [ { giveaway_id: 'a1b2c3', chat_id: chat.chat_id, title: '七月活跃抽奖', status: 'running', prizes: [{ name: '一等奖', count: 1, points_reward: 50 }], minimum_points: 20, entry_cost: 5, participation_reward: 2, ends_at: '2026-07-25T08:00:00Z', created_at: '2026-07-24T08:00:00Z', }, ], top_accounts: [{ chat_id: chat.chat_id, user_id: '10001', username: 'alice', display_name: '爱丽丝 示例', balance: 180, lifetime_earned: 220, lifetime_spent: 40 }], 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' }], }; } else if (path.endsWith('/chats')) { data = { items: [chat], total: 1, page: 1, page_size: 20 }; } else if (path === `/api/admin/v1/chats/${chat.chat_id}`) { data = { ...chat, description: '用于后台验收的测试群', permissions: {}, automation: { chat_id: chat.chat_id, auto_replies: [], blacklist_words: ['广告'], risk_rules: [ { rule_id: 'ads', name: '广告拦截', enabled: true, keywords: ['广告'], match_images: true, match_links: true, actions: ['delete', 'warn'], duration_seconds: 3600, }, ], identity_monitor: { enabled: true, notify_in_chat: false }, welcome: { enabled: false, text: '', media: null }, captcha_enabled: false, chatbot_enabled: false, antiflood_enabled: true, }, }; } else if (path.endsWith('/admins')) { data = { items: [] }; } else if (path.endsWith('/members/recent')) { data = { items: [recentMember] }; } else if (path.endsWith('/members/identity-changes')) { data = { items: [{ change_id: 'change-1', chat_id: chat.chat_id, user_id: '10001', before: { username: 'alice', display_name: '小明 同学' }, after: { username: 'alice_new', display_name: '小明 新昵称' }, changed_fields: ['username', 'display_name'], observed_at: '2026-07-24T08:30:00Z', }], total: 1, page: 1, page_size: 20, }; } else if (path.endsWith('/members/search')) { data = { items: [{ ...recentMember, status: 'member' }] }; } else if (path.endsWith('/giveaways')) { data = { items: [], total: 0, page: 1, page_size: 20 }; } else if (path.endsWith('/points/accounts') || path.endsWith('/points/transactions')) { data = { items: [], total: 0, page: 1, page_size: 20 }; } else if (path.endsWith('/audit-logs')) { data = { items: [], total: 0, page: 1, page_size: 20 }; } else if (path.endsWith('/directory/applications')) { data = { items: [{ user_id: '20001', username: 'teacher_demo', display_name: '示例技师', application_status: 'pending', listed: false, online: false, applied_at: '2026-07-24T08:00:00Z', location: { source: 'telegram_location', updated_at: '2026-07-24T08:00:00Z', }, }], total: 1, page: 1, page_size: 20, }; } else if ( path.endsWith('/directory/profiles') || path.endsWith('/directory/locations') || path.endsWith('/directory/events') ) { data = { items: [], total: 0, page: 1, page_size: 20 }; } else if (path.endsWith('/directory/settings')) { data = { online_duration_hours: 24, nearby_radius_km: 20, nearby_radius_options_km: [5, 10, 20, 50], }; } else if (path.endsWith('/directory/fulfillment-metrics')) { data = { counts: {}, valid_requests: 3, quoted_requests: 2, confirmed_requests: 1, request_submission_rate: 1, quote_rate: 0.6667, customer_confirmation_rate: 0.5, match_success_rate: 0.3333, fulfillment_rate: 1, cancellation_rate: 0, cancellation_distribution: { customer: 0, technician: 0 }, scope_note: '仅统计平台服务单;直接 Telegram 私聊成交不在统计范围。', }; } else if (path.endsWith('/directory/service-settings')) { data = { customer_max_open_orders: 3, customer_daily_request_limit: 10, review_qr_expiry_hours: 24, review_forum_chat_id: '-1001234567890', review_forum_username: 'public_reviews', }; } else if (path.endsWith('/directory/package-templates')) { data = { items: [], total: 0 }; } else if ( path.endsWith('/directory/service-orders') || path.endsWith('/directory/reviews') || path.endsWith('/directory/review-qrs') || path.endsWith('/directory/leaderboard') || path.endsWith('/directory/customer-blocks') || path.endsWith('/directory/reports') ) { data = { items: [], total: 0, page: 1, page_size: 20 }; } else if (path.endsWith('/directory/review-template')) { data = { template_id: 'review1', version: 1, status: 'active', published_at: '2026-07-26T00:00:00Z', questions: [ { question_id: 'effect', type: 'single_choice', label: '服务效果', required: true, weight: 100, options: [ { value: 'good', label: '满意', score: 5 }, { value: 'bad', label: '不满意', score: 1 }, ], }, ], }; } else if (path.endsWith('/business-assistant/status')) { data = { polling_state: 'running', business_mode_supported: true, webhook_conflict: false, webhook_url: '', model_configured: true, usage: { ai_calls: 8, customers: 3, conversations: 3, handoffs: 1, human_paused: 1 }, }; } else if (path.endsWith('/business-assistant/connections')) { data = { items: [{ connection_id: 'conn-a', user: { id: '900', username: 'owner', first_name: '店主' }, user_chat_id: '900', rights: { can_reply: true, can_read_messages: true }, is_enabled: true, last_event_at: '2026-08-14T08:00:00Z', }], total: 1, page: 1, page_size: 100, }; } else if (path.endsWith('/business-assistant/settings')) { data = { connection_id: 'conn-a', assistant_enabled: true, system_prompt: '只依据知识库回答。', language: 'zh-CN', tone: 'professional', account_daily_limit: 200, customer_daily_limit: 20, human_pause_hours: 24, notification_destination: 'both', ops_group_id: '-100123', timezone: 'Asia/Shanghai', digest_enabled: false, digest_time: '09:00', handoff_message: '请稍候,已转人工。', unsupported_message: '该消息需要人工处理。', }; } else if (path.endsWith('/business-assistant/knowledge')) { data = { items: [], total: 0, page: 1, page_size: 20 }; } else if (path.endsWith('/business-assistant/conversations')) { data = { items: [], total: 0, page: 1, page_size: 20 }; } else if (path.endsWith('/business-assistant/usage')) { data = { ai_calls: 8, customers: 3, conversations: 3, handoffs: 1, human_paused: 1 }; } else if (path.endsWith('/settings')) { data = { web_address: 'http://127.0.0.1:8088/admin', bind_host: '0.0.0.0', upload_limit_mb: 20, supervisor_mode: true, bot: { connected: false, id: '', username: '', name: '' }, }; } await route.fulfill({ status: 200, contentType: 'application/json', body: envelope(data) }); }); }); test('登录与主要管理视图在不同视口无页面级横向滚动', async ({ page }, testInfo) => { await page.goto('/admin/login'); await page.getByLabel('密码').fill('qwe0.123456'); await page.getByRole('button', { name: /登\s*录/ }).click(); await expect(page.getByRole('heading', { name: '概览' })).toBeVisible(); const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth); expect(overflow).toBeLessThanOrEqual(1); await page.screenshot({ path: testInfo.outputPath('dashboard.png'), fullPage: true }); await page.goto('/admin/chats'); await expect(page.getByText('产品运营测试群')).toBeVisible(); expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1); await page.goto(`/admin/chats/${chat.chat_id}`); await selectChatWorkspace(page, '自动化与风控'); await expect(page.getByRole('heading', { name: '内容风控' })).toBeVisible(); await expect(page.getByText('广告拦截')).toBeVisible(); await page.getByRole('button', { name: '编辑 广告拦截' }).click(); await expect(page.getByText('编辑风控规则')).toBeVisible(); await expect(page.getByLabel('封禁时长')).toHaveValue('60'); expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1); await page.getByRole('button', { name: '关闭' }).click(); await selectChatWorkspace(page, '成员与管理员'); await expect(page.getByText('小明 同学', { exact: true }).first()).toBeVisible(); await expect(page.getByText('小明 新昵称', { exact: true }).first()).toBeVisible(); expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1); await page.goto('/admin/giveaways'); await expect(page.getByRole('heading', { name: '抽奖管理' })).toBeVisible(); expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1); for (const [path, heading] of [ ['/admin/points', '积分管理'], ['/admin/directory', '技师与附近'], ['/admin/services', '技师咨询与评价'], ['/admin/business-assistant', '智能接待'], ['/admin/audit', '操作日志'], ['/admin/settings', '系统设置'], ] as const) { await page.goto(path); await expect(page.getByRole('heading', { name: heading })).toBeVisible(); if (path === '/admin/settings') { await expect(page.getByText('全部职责').first()).toBeVisible(); await page.screenshot({ path: testInfo.outputPath('settings.png'), fullPage: true }); } if (path === '/admin/directory') { await expect(page.getByText('示例技师')).toBeVisible(); await expect(page.getByText('位置属于敏感信息')).toBeVisible(); await page.screenshot({ path: testInfo.outputPath('directory.png'), fullPage: true }); } if (path === '/admin/services') { await expect(page.getByText('咨询平台模式')).toBeVisible(); await page.screenshot({ path: testInfo.outputPath('services.png'), fullPage: true }); } if (path === '/admin/business-assistant') { await expect(page.getByText('允许回复')).toBeVisible(); await expect(page.getByText('允许标记已读')).toBeVisible(); await page.screenshot({ path: testInfo.outputPath('business-assistant.png'), fullPage: true }); } expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1); } });