feat: release contextual omnichannel customer platform
Add Telegram, WhatsApp and email account management, realtime messaging, contextual multilingual AI, customer intelligence, knowledge workflows, enterprise controls, a unified design system and complete repository documentation.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
const password = 'Lingxi-Design-2026!'
|
||||
|
||||
async function login(page: Page, suffix: string) {
|
||||
const account = `design-${suffix}-${Date.now()}@local.test`
|
||||
const registration = await page.request.post('/api/auth/register', {
|
||||
data: {
|
||||
name: 'Design Audit',
|
||||
account,
|
||||
password,
|
||||
company: 'Design Audit',
|
||||
industry: '跨境电商',
|
||||
teamSize: '1-10人',
|
||||
},
|
||||
})
|
||||
expect(registration.ok()).toBeTruthy()
|
||||
await page.goto('/login')
|
||||
await expect(page.locator('img[src="/brand-mark.svg"]')).toHaveCount(2)
|
||||
await page.locator('input[autocomplete="username"]').fill(account)
|
||||
await page.locator('input[autocomplete="current-password"]').fill(password)
|
||||
await page.getByRole('button', { name: '登录' }).click()
|
||||
await expect(page).toHaveURL(/\/dashboard/)
|
||||
await expect(page.getByText('今日工作概览')).toBeVisible()
|
||||
}
|
||||
|
||||
test('统一设计系统桌面端页面验收', async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name !== 'desktop-chromium', '仅在桌面视口执行')
|
||||
await login(page, 'desktop')
|
||||
|
||||
const pages = [
|
||||
['/dashboard', '今日工作概览', 'dashboard'],
|
||||
['/inbox', '消息', 'inbox'],
|
||||
['/channels', '渠道与账号', 'channels'],
|
||||
['/customers', '客户管理', 'customers'],
|
||||
['/opportunities', '商机与跟进', 'opportunities'],
|
||||
['/knowledge', '企业知识库', 'knowledge'],
|
||||
['/analytics', '业务分析', 'analytics'],
|
||||
['/settings', '管理企业资料、通知、智能能力与账号安全', 'settings'],
|
||||
] as const
|
||||
|
||||
for (const [url, marker, name] of pages) {
|
||||
await page.goto(url)
|
||||
await expect(page.getByText(marker, { exact: false }).first()).toBeVisible()
|
||||
await expect(page.getByText('Unexpected Application Error!', { exact: true })).toHaveCount(0)
|
||||
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1)).toBe(true)
|
||||
await page.screenshot({ path: testInfo.outputPath(`${name}.png`), fullPage: name !== 'inbox' })
|
||||
}
|
||||
|
||||
await page.goto('/analytics')
|
||||
await page.getByRole('button', { name: '近 7 天', exact: true }).click()
|
||||
await expect(page.getByRole('button', { name: '近 7 天', exact: true })).toHaveAttribute('aria-pressed', 'true')
|
||||
|
||||
await page.goto('/opportunities?tab=tasks')
|
||||
await expect(page).toHaveURL(/tab=tasks/)
|
||||
await expect(page.getByText('任务', { exact: false }).first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('统一设计系统移动端页面验收', async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name !== 'mobile-chromium', '仅在移动视口执行')
|
||||
await login(page, 'mobile')
|
||||
|
||||
await page.screenshot({ path: testInfo.outputPath('dashboard-mobile.png'), fullPage: true })
|
||||
await page.goto('/settings')
|
||||
await expect(page.getByLabel('设置分类')).toBeVisible()
|
||||
await page.getByLabel('设置分类').selectOption('ai')
|
||||
await expect(page.getByLabel('设置分类')).toHaveValue('ai')
|
||||
await expect(page.getByText('控制回复建议、分析和发送前确认规则')).toBeVisible()
|
||||
await page.screenshot({ path: testInfo.outputPath('settings-mobile.png'), fullPage: true })
|
||||
|
||||
await page.goto('/inbox')
|
||||
await expect(page.getByText('消息', { exact: true }).first()).toBeVisible()
|
||||
await expect(page.getByText('Unexpected Application Error!', { exact: true })).toHaveCount(0)
|
||||
await page.screenshot({ path: testInfo.outputPath('inbox-mobile.png'), fullPage: false })
|
||||
})
|
||||
@@ -0,0 +1,123 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
const account = process.env.E2E_ACCOUNT || 'e2e-1785008967@local.test'
|
||||
const password = process.env.E2E_PASSWORD || 'Lingxi-E2E-2026!'
|
||||
|
||||
test('知识库与逐句意图在真实页面可用', async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name !== 'desktop-chromium', '仅在桌面视口执行')
|
||||
|
||||
const loginResponse = await page.request.post('/api/auth/login', {
|
||||
data: { account, password },
|
||||
})
|
||||
expect(loginResponse.ok()).toBeTruthy()
|
||||
const loginPayload = await loginResponse.json()
|
||||
const token = loginPayload.data.token as string
|
||||
const headers = { Authorization: `Bearer ${token}` }
|
||||
|
||||
await page.goto('/login')
|
||||
await page.locator('input[autocomplete="username"]').fill(account)
|
||||
await page.locator('input[autocomplete="current-password"]').fill(password)
|
||||
await page.getByRole('button', { name: '登录' }).click()
|
||||
await expect(page).toHaveURL(/\/dashboard/)
|
||||
|
||||
await page.goto('/knowledge')
|
||||
await expect(page.getByText('已分段索引', { exact: true })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: '上传文件' })).toBeVisible()
|
||||
await page.screenshot({ path: testInfo.outputPath('knowledge-index.png'), fullPage: true })
|
||||
|
||||
const conversationsResponse = await page.request.get('/api/conversations', { headers })
|
||||
const conversations = (await conversationsResponse.json()).data.list as Array<{ id: string }>
|
||||
let conversationId = ''
|
||||
let messageId = ''
|
||||
let messageCount = 0
|
||||
for (const conversation of conversations.slice(0, 20)) {
|
||||
const messagesResponse = await page.request.get(
|
||||
`/api/conversations/${encodeURIComponent(conversation.id)}/messages`,
|
||||
{ headers },
|
||||
)
|
||||
const messages = (await messagesResponse.json()).data as Array<{
|
||||
id: string
|
||||
sender: string
|
||||
type: string
|
||||
originalText: string
|
||||
intentAnalysis?: unknown
|
||||
}>
|
||||
const candidate = [...messages].reverse().find((message) => (
|
||||
message.sender === 'customer'
|
||||
&& message.type === 'text'
|
||||
&& /[\p{L}]/u.test(message.originalText || '')
|
||||
))
|
||||
if (candidate) {
|
||||
conversationId = conversation.id
|
||||
messageId = candidate.id
|
||||
messageCount = messages.length
|
||||
break
|
||||
}
|
||||
}
|
||||
expect(conversationId).not.toBe('')
|
||||
const aiStatusResponse = await page.request.get(
|
||||
`/api/conversations/${encodeURIComponent(conversationId)}/ai-status`,
|
||||
{ headers },
|
||||
)
|
||||
expect(aiStatusResponse.ok()).toBeTruthy()
|
||||
const aiStatus = (await aiStatusResponse.json()).data
|
||||
expect(['not-started', 'queued', 'running', 'completed']).toContain(aiStatus.backfill.status)
|
||||
if (messageCount > 80) {
|
||||
await expect.poll(async () => {
|
||||
const response = await page.request.get(
|
||||
`/api/conversations/${encodeURIComponent(conversationId)}/ai-status`,
|
||||
{ headers },
|
||||
)
|
||||
return (await response.json()).data.context.status
|
||||
}, { timeout: 30_000 }).toBe('ready')
|
||||
const readyResponse = await page.request.get(
|
||||
`/api/conversations/${encodeURIComponent(conversationId)}/ai-status`,
|
||||
{ headers },
|
||||
)
|
||||
const readyStatus = (await readyResponse.json()).data
|
||||
expect(readyStatus.context.sourceMessageCount).toBeGreaterThan(0)
|
||||
expect(readyStatus.context.memory.summary).toBeTruthy()
|
||||
}
|
||||
const analyzeResponse = await page.request.post(
|
||||
`/api/conversations/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(messageId)}/analyze`,
|
||||
{ headers, data: { force: true } },
|
||||
)
|
||||
expect(analyzeResponse.ok()).toBeTruthy()
|
||||
|
||||
await page.goto(`/inbox?c=${encodeURIComponent(conversationId)}`)
|
||||
await expect(page.getByTestId('conversation-insight-bar')).toBeVisible()
|
||||
await expect(page.getByTestId('conversation-intent-toggle')).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole('switch', { name: '当前会话逐句意图识别' }),
|
||||
).toHaveAttribute('aria-checked', 'true')
|
||||
await expect.poll(() => page.getByTestId('message-intent').count()).toBeGreaterThan(0)
|
||||
const intentCount = await page.getByTestId('message-intent').count()
|
||||
const visibleIntent = page.getByTestId('message-intent').nth(intentCount - 1)
|
||||
await visibleIntent.scrollIntoViewIfNeeded()
|
||||
await expect(visibleIntent).toBeVisible()
|
||||
const intentSwitch = page.getByRole('switch', { name: '当前会话逐句意图识别' })
|
||||
await intentSwitch.click()
|
||||
await expect(intentSwitch).toHaveAttribute('aria-checked', 'false')
|
||||
await expect(page.getByTestId('message-intent')).toHaveCount(0)
|
||||
await page.reload()
|
||||
await expect(
|
||||
page.getByRole('switch', { name: '当前会话逐句意图识别' }),
|
||||
).toHaveAttribute('aria-checked', 'false')
|
||||
await page.getByRole('switch', { name: '当前会话逐句意图识别' }).click()
|
||||
await expect(
|
||||
page.getByRole('switch', { name: '当前会话逐句意图识别' }),
|
||||
).toHaveAttribute('aria-checked', 'true')
|
||||
await expect.poll(() => page.getByTestId('message-intent').count()).toBeGreaterThan(0)
|
||||
await expect(page.getByText('Unexpected Application Error!', { exact: true })).toHaveCount(0)
|
||||
await page.screenshot({ path: testInfo.outputPath('message-intent.png'), fullPage: false })
|
||||
|
||||
await page.goto('/settings')
|
||||
await page.getByRole('button', { name: '智能回复' }).click()
|
||||
const globalIntentSwitch = page.getByRole('switch', { name: '逐条客户消息意图识别' })
|
||||
await expect(globalIntentSwitch).toBeVisible()
|
||||
await globalIntentSwitch.click()
|
||||
await expect(globalIntentSwitch).toHaveAttribute('aria-checked', 'false')
|
||||
await globalIntentSwitch.click()
|
||||
await expect(globalIntentSwitch).toHaveAttribute('aria-checked', 'true')
|
||||
await expect(page.getByText('invalid input syntax for type json')).toHaveCount(0)
|
||||
})
|
||||
@@ -177,6 +177,16 @@ test('登录与核心工作台视觉链路', async ({ page }, testInfo) => {
|
||||
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\]/)
|
||||
const targetLanguage = page.getByLabel('客户目标语言')
|
||||
await expect(targetLanguage).toBeVisible()
|
||||
if (testInfo.project.name !== 'mobile-chromium') {
|
||||
await targetLanguage.selectOption('es')
|
||||
await expect(targetLanguage).toHaveValue('es')
|
||||
await page.reload()
|
||||
await expect(page.getByLabel('客户目标语言')).toHaveValue('es')
|
||||
await page.getByLabel('客户目标语言').selectOption('auto')
|
||||
await expect(page.getByLabel('客户目标语言')).toHaveValue('auto')
|
||||
}
|
||||
await page.screenshot({ path: testInfo.outputPath('inbox.png'), fullPage: true })
|
||||
})
|
||||
|
||||
@@ -205,7 +215,8 @@ test('conversation list reorders immediately from WebSocket events', async ({ pa
|
||||
await page.goto('/inbox')
|
||||
const items = page.getByTestId('conversation-item')
|
||||
await expect(items.nth(1)).toBeVisible()
|
||||
await expect.poll(() => sockets.length).toBeGreaterThanOrEqual(2)
|
||||
// 页面跳转可能复用现有应用外壳,也可能重新建立连接;只要求当前至少存在一条可用连接。
|
||||
await expect.poll(() => sockets.length).toBeGreaterThanOrEqual(1)
|
||||
|
||||
const target = items.nth(1)
|
||||
const targetId = await target.getAttribute('data-conversation-id')
|
||||
@@ -245,10 +256,10 @@ test('知识文件真实上传解析', async ({ page }, testInfo) => {
|
||||
await page.locator('input[type="file"]').setInputFiles({
|
||||
name: `visual-e2e-${testInfo.project.name}.txt`,
|
||||
mimeType: 'text/plain',
|
||||
buffer: Buffer.from('灵犀跨境视觉 E2E 知识解析测试。'),
|
||||
buffer: Buffer.from('灵犀跨境视觉 E2E 知识解析测试,覆盖产品资料与售后政策。'),
|
||||
})
|
||||
await page.getByRole('button', { name: '上传并解析' }).click()
|
||||
await expect(page.getByText(/知识文件解析完成/)).toBeVisible()
|
||||
await page.getByRole('button', { name: /上传.*解析.*索引/ }).click()
|
||||
await expect(page.getByText(/文件解析并建立检索索引/)).toBeVisible()
|
||||
await page.screenshot({ path: testInfo.outputPath('knowledge-uploaded.png'), fullPage: true })
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user