import { expect, test, type Page, type WebSocketRoute } from '@playwright/test' const account = process.env.E2E_ACCOUNT || 'e2e-1785008967@local.test' const password = process.env.E2E_PASSWORD || 'Lingxi-E2E-2026!' async function login(page: Page, loginAccount = account, loginPassword = password) { await page.goto('/login') await page.locator('input[autocomplete="username"]').fill(loginAccount) await page.locator('input[autocomplete="current-password"]').fill(loginPassword) await page.locator('button[type="submit"]').click() await expect(page).toHaveURL(/\/dashboard$/) } test('Telegram 手机号验证码和二步密码登录向导', async ({ page, request }, testInfo) => { const suffix = `${Date.now()}-${testInfo.project.name}` const loginAccount = `channel-e2e-${suffix}@local.test` const loginPassword = 'Lingxi-Channel-2026!' const response = await request.post('/api/auth/register', { data: { name: 'Channel E2E', account: loginAccount, password: loginPassword, company: 'Channel E2E', industry: '跨境电商', teamSize: '1-10人' }, }) expect(response.ok()).toBeTruthy() await login(page, loginAccount, loginPassword) await page.goto('/channels?setup=telegram') await expect(page.getByTestId('telegram-onboarding')).toContainText('先绑定 Telegram') await expect(page.getByPlaceholder('+8613800138000')).toBeVisible() await expect(page.getByLabel('Telegram 验证码', { exact: true })).toHaveCount(0) const sendTelegramCode = page.getByRole('button', { name: '发送 Telegram 验证码' }) await expect(sendTelegramCode).toBeVisible() await sendTelegramCode.click() await expect(page.getByText('请输入国际格式手机号,例如 +8613800138000')).toBeVisible() await expect(page.getByPlaceholder('+8613800138000')).toBeVisible() await expect(page.getByText('每个企业独立绑定账号')).toBeVisible() await page.screenshot({ path: testInfo.outputPath('telegram-login-wizard.png'), fullPage: true }) }) test('企业邮箱 IMAP 和 SMTP 连接向导', async ({ page, request }, testInfo) => { const suffix = `${Date.now()}-${testInfo.project.name}` const loginAccount = `email-e2e-${suffix}@local.test` const loginPassword = 'Lingxi-Email-2026!' const response = await request.post('/api/auth/register', { data: { name: 'Email E2E', account: loginAccount, password: loginPassword, company: 'Email E2E', industry: '跨境电商', teamSize: '1-10人' }, }) expect(response.ok()).toBeTruthy() await login(page, loginAccount, loginPassword) await page.goto('/channels') const emailCard = page.getByTestId('channel-card-email') await emailCard.getByRole('button', { name: '连接', exact: true }).click() await expect(page.getByText('IMAP 收信 + SMTP 发信', { exact: true })).toBeVisible() await expect(page.getByPlaceholder('sales@company.com')).toBeVisible() await expect(page.getByPlaceholder('仅提交到本地服务端验证')).toHaveAttribute('type', 'password') await expect(page.getByRole('button', { name: '验证并连接邮箱' })).toBeVisible() await page.screenshot({ path: testInfo.outputPath('email-connection-wizard.png'), fullPage: true }) }) test('登录与核心工作台视觉链路', async ({ page }, testInfo) => { await login(page) await expect(page).toHaveURL(/\/dashboard$/) await page.screenshot({ path: testInfo.outputPath('dashboard.png'), fullPage: true }) await page.goto('/channels') await expect(page.getByText('Telegram', { exact: true }).first()).toBeVisible() await page.screenshot({ path: testInfo.outputPath('channels.png'), fullPage: true }) await page.goto('/inbox') await page.getByRole('button', { name: '添加 Telegram 用户或群组' }).click() await expect(page.getByRole('dialog')).toContainText('添加 Telegram 会话') await expect(page.getByLabel('用户名、手机号或用户 ID')).toBeVisible() await page.getByRole('button', { name: '群组/频道', exact: true }).click() await expect(page.getByLabel('群组/频道用户名或 Chat ID')).toBeVisible() await page.getByRole('button', { name: '邀请链接', exact: true }).click() await expect(page.getByLabel('Telegram 邀请链接')).toBeVisible() await page.getByRole('button', { name: '关闭', exact: true }).click() const privateConversation = page.locator( '[data-testid="conversation-item"][data-channel="telegram"]:not([data-telegram-chat-type="channel"])', ).first() await privateConversation.click() await expect(page.getByTestId('message-content')).toHaveClass(/justify-end/) const languageSwitcher = page.getByRole('radiogroup', { name: '消息显示语言' }) const originalMode = languageSwitcher.getByRole('radio', { name: '原文', exact: true }) const translatedMode = languageSwitcher.getByRole('radio', { name: '译文', exact: true }) await originalMode.click() await expect(originalMode).toHaveAttribute('aria-checked', 'true') await translatedMode.click() await expect(translatedMode).toHaveAttribute('aria-checked', 'true') await expect.poll(() => page.evaluate(() => localStorage.getItem('lingxi-message-view-mode'))).toBe('translated') await expect(page.locator('input[type="file"]')).toHaveCount(4) const outgoingSurface = page.locator('[data-testid="message-surface"][data-direction="outgoing"]') await expect(outgoingSurface.first()).toHaveClass(/telegram-bubble-out/) const incomingSurface = page.locator('[data-testid="message-surface"][data-direction="incoming"]') if (await incomingSurface.count()) await expect(incomingSurface.first()).toHaveClass(/telegram-bubble-in/) await expect(privateConversation).toHaveClass(/conversation-selected/) await expect(page.getByTestId('conversation-list-panel')).toHaveClass(/border-slate-300/) await expect(page.getByTestId('chat-panel')).toHaveClass(/bg-\[#eaf2f9\]/) await expect(page.getByTestId('conversation-insight-bar').locator('img')).toHaveCount(0) await expect(page.getByTestId('conversation-insight-bar')).toContainText('AI 客户洞察') await page.getByRole('button', { name: 'Telegram 会话操作' }).click() await expect(page.getByText(/置顶会话|取消置顶/)).toBeVisible() await expect(page.getByText(/归档会话|移出归档/)).toBeVisible() await expect(page.getByText(/静音 8 小时|恢复通知/)).toBeVisible() await expect(page.getByText('清空消息', { exact: true })).toBeVisible() await expect(page.getByText('删除会话', { exact: true })).toBeVisible() await page.keyboard.press('Escape') await page.getByRole('button', { name: '切换聊天背景' }).click() await page.getByText('极光紫', { exact: true }).click() await expect(page.getByTestId('message-list')).toHaveClass(/chat-wallpaper-aurora/) await page.getByRole('button', { name: '切换聊天背景' }).click() await page.getByText('日落橙', { exact: true }).click() await expect(page.getByTestId('message-list')).toHaveClass(/chat-wallpaper-sunset/) await page.getByRole('button', { name: '切换聊天背景' }).click() await page.getByText('薄荷绿', { exact: true }).click() await expect(page.getByTestId('message-list')).toHaveClass(/chat-wallpaper-mint/) await expect(page.getByPlaceholder('输入回复内容,Enter 发送,Shift+Enter 换行')).toHaveClass(/min-h-\[86px\]/) await page.screenshot({ path: testInfo.outputPath('inbox.png'), fullPage: true }) }) test('mobile conversation swipe reveals Telegram actions', async ({ page }, testInfo) => { test.skip(testInfo.project.name !== 'mobile-chromium') await login(page) await page.goto('/inbox') const item = page.locator('[data-testid="conversation-item"][data-channel="telegram"]').first() await expect(item).toBeVisible() await item.dispatchEvent('touchstart', { touches: [{ identifier: 1, clientX: 320, clientY: 120 }] }) await item.dispatchEvent('touchmove', { touches: [{ identifier: 1, clientX: 120, clientY: 120 }] }) await page.waitForTimeout(100) await item.dispatchEvent('touchend', { changedTouches: [{ identifier: 1, clientX: 120, clientY: 120 }] }) const actions = item.locator('xpath=..').getByTestId('conversation-swipe-actions') await expect(actions).toHaveClass(/opacity-100/) await expect(actions.getByRole('button', { name: '删除' })).toBeVisible() }) test('conversation list reorders immediately from WebSocket events', async ({ page }) => { const sockets: WebSocketRoute[] = [] await page.routeWebSocket(/\/api\/ws/, (socket) => { sockets.push(socket) socket.send(JSON.stringify({ type: 'connected', at: new Date().toISOString() })) }) await login(page) await page.goto('/inbox') const items = page.getByTestId('conversation-item') await expect(items.nth(1)).toBeVisible() await expect.poll(() => sockets.length).toBeGreaterThanOrEqual(2) const target = items.nth(1) const targetId = await target.getAttribute('data-conversation-id') expect(targetId).toBeTruthy() const message = { id: `realtime-${Date.now()}`, conversationId: targetId, sender: 'customer', type: 'text', originalText: 'Realtime list regression', originalLang: 'en', status: 'delivered', createdAt: new Date(Date.now() + 60_000).toISOString(), } sockets.at(-1)!.send(JSON.stringify({ type: 'message.received', conversationId: targetId, message, conversation: { id: targetId, unreadCount: 7, status: 'pending', lastMessage: message, updatedAt: message.createdAt, }, })) await expect(items.first()).toHaveAttribute('data-conversation-id', targetId!) await expect(items.first()).toContainText('Realtime list regression') await expect(items.first()).toContainText('7') }) test('知识文件真实上传解析', async ({ page }, testInfo) => { await login(page) await page.goto('/knowledge') await page.getByRole('button', { name: '上传文件' }).click() await page.locator('input[type="file"]').setInputFiles({ name: `visual-e2e-${testInfo.project.name}.txt`, mimeType: 'text/plain', buffer: Buffer.from('灵犀跨境视觉 E2E 知识解析测试。'), }) await page.getByRole('button', { name: '上传并解析' }).click() await expect(page.getByText(/知识文件解析完成/)).toBeVisible() await page.screenshot({ path: testInfo.outputPath('knowledge-uploaded.png'), fullPage: true }) }) test('客户画像雷达与采购情报可视化', async ({ page }, testInfo) => { await login(page) await page.goto('/customers') const rows = page.getByTestId('customer-row') await expect(rows.first()).toBeVisible() await rows.first().getByRole('cell').nth(1).click() await expect(page.getByText('客户价值雷达', { exact: true })).toBeVisible() await expect(page.getByText('AI 客户洞察', { exact: true })).toBeVisible() await expect(page.getByText('采购情报', { exact: true })).toBeVisible() await expect(page.getByText('标签画像', { exact: true })).toBeVisible() const semanticTags = page.locator('[data-tag-category]') if (await semanticTags.count()) await expect(semanticTags.first()).toBeVisible() else await expect(page.getByText('暂无标签,AI 将在收到有效客户消息后自动补全。')).toBeVisible() await page.screenshot({ path: testInfo.outputPath('customer-profile-radar.png'), fullPage: true }) }) test('团队邀请和支付页不显示虚假成功', async ({ page }, testInfo) => { await login(page) await page.goto('/team') await expect(page.getByRole('button', { name: '邀请成员' })).toBeVisible() await page.screenshot({ path: testInfo.outputPath('team.png'), fullPage: true }) await page.goto('/billing') await expect(page.getByText('Stripe Checkout', { exact: false })).toBeVisible() await page.screenshot({ path: testInfo.outputPath('billing.png'), fullPage: true }) })