- 新增 BUG_TRACKER.md 记录已修复和开放的bug - 新增 TEST_EXECUTION_2026-04-30.md 自动化测试执行报告 - 新增 TEST_PROGRESS.md 测试进度跟踪文档 - 修复 Android SDK connectedCheck 内存不足问题 - 修复 Android sample-app CAMERA 权限 lint 失败 - 修复 Android UpdateSDK longVersionCode minSdk lint 失败 - 修复 RN Chat Demo Jest 无法解析本地 SDK 源码包 - 修复 Python Server SDK 回调消息解析与顶层导出错误 - 修复 Vue3 SDK package exports 条件顺序警告 - 修复 im-service 群消息不进入会话聚合列表问题 - 修复 im-service 对外时间字段单位不一致问题 - 修复 RN SDK 历史消息 upsert 丢失回推状态问题 - 修复 Android 黑名单操作静默失败问题 - 修复 AppSecret 调用无鉴权安全问题 - 修复 IM Token 无过期信息问题 - 修复 RN SDK 草稿同步服务端数据污染问题 - 修复 Vue3 SDK 撤回编辑后依赖 WS 刷新延迟问题 - 修复 im-service 消息摘要不支持媒体类型问题
403 行
13 KiB
JavaScript
403 行
13 KiB
JavaScript
/**
|
|
* Vue3 SDK 端到端集成测试
|
|
* 直接运行: node __tests__/integration/e2e.node.mjs
|
|
* 环境要求: Node.js >= 22 (原生支持 fetch + WebSocket)
|
|
*/
|
|
|
|
import { init, setToken, setUserId, getToken, getUserId, getConfig } from '../../dist/index.es.js'
|
|
import {
|
|
http,
|
|
ImClient,
|
|
sendMessage,
|
|
createGroup,
|
|
listConversations,
|
|
fetchHistory,
|
|
fetchGroupHistory,
|
|
markRead,
|
|
listFriends,
|
|
listGroups,
|
|
getProfile,
|
|
updateProfile,
|
|
sendFriendRequest,
|
|
listFriendRequests,
|
|
acceptFriendRequest,
|
|
rejectFriendRequest,
|
|
searchUsers,
|
|
searchGroups,
|
|
searchMessages,
|
|
getGroupInfo,
|
|
editMessage,
|
|
revokeMessage,
|
|
deleteConversation,
|
|
setConversationPinned,
|
|
setConversationMuted,
|
|
setDraft,
|
|
} from '../../dist/index.es.js'
|
|
|
|
const BASE_URL = 'http://192.168.113.37:8082'
|
|
const WS_URL = 'ws://192.168.113.37:8082/ws/im'
|
|
const DEMO_AUTH_URL = 'http://192.168.113.37:8085/api/demo/auth'
|
|
|
|
const COLORS = {
|
|
reset: '\x1b[0m',
|
|
green: '\x1b[32m',
|
|
red: '\x1b[31m',
|
|
yellow: '\x1b[33m',
|
|
cyan: '\x1b[36m',
|
|
}
|
|
|
|
let passed = 0
|
|
let failed = 0
|
|
const errors = []
|
|
|
|
function ok(desc) {
|
|
passed++
|
|
console.log(`${COLORS.green}✓${COLORS.reset} ${desc}`)
|
|
}
|
|
|
|
function fail(desc, err) {
|
|
failed++
|
|
console.log(`${COLORS.red}✗${COLORS.reset} ${desc}`)
|
|
console.log(` ${COLORS.red}${err.message || err}${COLORS.reset}`)
|
|
errors.push({ desc, error: err.message || String(err) })
|
|
}
|
|
|
|
async function section(name) {
|
|
console.log(`\n${COLORS.cyan}▶ ${name}${COLORS.reset}`)
|
|
}
|
|
|
|
// ── 工具 ──────────────────────────────
|
|
|
|
async function login(userId, password = '123456') {
|
|
const res = await fetch(`${DEMO_AUTH_URL}/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ appId: 'ak_demo_chat', userId, password }),
|
|
})
|
|
const json = await res.json()
|
|
if (json.code !== 200) throw new Error(`Login failed: ${json.message}`)
|
|
return json.data.imToken
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise((r) => setTimeout(r, ms))
|
|
}
|
|
|
|
function waitForEvent(client, event, timeoutMs = 10000) {
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => reject(new Error(`Timeout waiting for ${event}`)), timeoutMs)
|
|
client.on(event, (...args) => {
|
|
clearTimeout(timer)
|
|
resolve(args)
|
|
})
|
|
})
|
|
}
|
|
|
|
// ── 主测试流程 ─────────────────────────
|
|
|
|
async function main() {
|
|
console.log(`${COLORS.cyan}=== XuqmGroup Vue3 SDK E2E Test ===${COLORS.reset}`)
|
|
console.log(`Server: ${BASE_URL}`)
|
|
console.log(`WebSocket: ${WS_URL}`)
|
|
|
|
let tokenA, tokenB
|
|
let clientA, clientB
|
|
|
|
try {
|
|
// ── 1. 登录 ──
|
|
await section('1. Authentication')
|
|
tokenA = await login('user_a')
|
|
tokenB = await login('user_b')
|
|
ok('user_a login success')
|
|
ok('user_b login success')
|
|
|
|
// ── 2. SDK 初始化 ──
|
|
await section('2. SDK Initialization')
|
|
init({ appKey: 'ak_demo_chat', appSecret: 'secret', debug: false, baseUrl: BASE_URL, wsUrl: WS_URL })
|
|
setToken(tokenA)
|
|
setUserId('user_a')
|
|
ok('init() with custom baseUrl/wsUrl')
|
|
if (getToken() === tokenA) ok('getToken() returns correct token')
|
|
if (getUserId() === 'user_a') ok('getUserId() returns correct userId')
|
|
if (getConfig().appKey === 'ak_demo_chat') ok('getConfig() returns appKey')
|
|
|
|
// ── 3. HTTP API: User Profile ──
|
|
await section('3. User Profile API')
|
|
try {
|
|
const profile = await getProfile('user_a')
|
|
ok(`getProfile() → ${profile.userId}, nickname=${profile.nickname}`)
|
|
} catch (err) { fail('getProfile()', err) }
|
|
|
|
try {
|
|
const updated = await updateProfile('user_a', `TestUser_${Date.now()}`, null, 'MALE')
|
|
ok(`updateProfile() → nickname=${updated.nickname}`)
|
|
} catch (err) { fail('updateProfile()', err) }
|
|
|
|
// ── 4. HTTP API: Conversations ──
|
|
await section('4. Conversations API')
|
|
let conversations = []
|
|
try {
|
|
conversations = await listConversations(20)
|
|
ok(`listConversations() → ${conversations.length} items`)
|
|
} catch (err) { fail('listConversations()', err) }
|
|
|
|
if (conversations.length > 0) {
|
|
const conv = conversations[0]
|
|
try {
|
|
await markRead(conv.targetId, conv.chatType)
|
|
ok(`markRead(${conv.targetId}) success`)
|
|
} catch (err) { fail('markRead()', err) }
|
|
}
|
|
|
|
// ── 5. HTTP API: Message History ──
|
|
await section('5. Message History API')
|
|
try {
|
|
const hist = await fetchHistory('user_b', { page: 0, size: 10 })
|
|
ok(`fetchHistory() → ${hist.content.length} messages, total=${hist.totalElements}`)
|
|
} catch (err) { fail('fetchHistory()', err) }
|
|
|
|
try {
|
|
const search = await searchMessages('test', 'SINGLE', 'TEXT')
|
|
ok(`searchMessages() → ${search.content.length} results`)
|
|
} catch (err) { fail('searchMessages()', err) }
|
|
|
|
// ── 6. HTTP API: Friends ──
|
|
await section('6. Friends API')
|
|
try {
|
|
const friends = await listFriends()
|
|
ok(`listFriends() → ${friends.length} friends`)
|
|
} catch (err) { fail('listFriends()', err) }
|
|
|
|
try {
|
|
const reqs = await listFriendRequests('incoming')
|
|
ok(`listFriendRequests() → ${reqs.length} requests`)
|
|
} catch (err) { fail('listFriendRequests()', err) }
|
|
|
|
try {
|
|
const users = await searchUsers('user')
|
|
ok(`searchUsers() → ${users.length} users`)
|
|
} catch (err) { fail('searchUsers()', err) }
|
|
|
|
// ── 7. HTTP API: Groups ──
|
|
await section('7. Groups API')
|
|
let groups = []
|
|
try {
|
|
groups = await listGroups()
|
|
ok(`listGroups() → ${groups.length} groups`)
|
|
} catch (err) { fail('listGroups()', err) }
|
|
|
|
try {
|
|
const gsearch = await searchGroups('test')
|
|
ok(`searchGroups() → ${gsearch.length} groups`)
|
|
} catch (err) { fail('searchGroups()', err) }
|
|
|
|
if (groups.length > 0) {
|
|
try {
|
|
const info = await getGroupInfo(groups[0].id)
|
|
ok(`getGroupInfo() → ${info.name}`)
|
|
} catch (err) { fail('getGroupInfo()', err) }
|
|
}
|
|
|
|
// ── 8. WebSocket: Connection ──
|
|
await section('8. WebSocket Connection')
|
|
clientA = new ImClient()
|
|
clientB = new ImClient()
|
|
|
|
// Debug listeners
|
|
clientA.on('message', (msg) => console.log(`[DEBUG-A] msg=${msg.id} status=${msg.status} content=${msg.content?.substring(0,20)}`))
|
|
clientA.on('error', (e) => console.log(`[DEBUG-A] error`, e))
|
|
clientB.on('message', (msg) => console.log(`[DEBUG-B] msg=${msg.id} status=${msg.status} content=${msg.content?.substring(0,20)}`))
|
|
clientB.on('error', (e) => console.log(`[DEBUG-B] error`, e))
|
|
|
|
// Must register listeners BEFORE connect() to avoid race condition
|
|
const pA = waitForEvent(clientA, 'connected', 10000)
|
|
const pB = waitForEvent(clientB, 'connected', 10000)
|
|
|
|
clientA.connect()
|
|
setToken(tokenB)
|
|
setUserId('user_b')
|
|
clientB.connect()
|
|
|
|
await pA
|
|
ok('clientA WebSocket connected')
|
|
await pB
|
|
ok('clientB WebSocket connected')
|
|
await sleep(1500) // wait for subscriptions to settle
|
|
|
|
// ── 9. WebSocket: Send & Verify via HTTP History ──
|
|
await section('9. WebSocket Message Flow')
|
|
|
|
// Switch back to user_a for sending
|
|
setToken(tokenA)
|
|
setUserId('user_a')
|
|
|
|
const testContent = `WS_test_${Date.now()}`
|
|
const msg = clientA.send({
|
|
toId: 'user_b',
|
|
chatType: 'SINGLE',
|
|
msgType: 'TEXT',
|
|
content: testContent,
|
|
})
|
|
ok(`WS send() returned message id=${msg.id}, status=${msg.status}`)
|
|
|
|
// Wait for sender echo (server echoes message back to sender with SENT status)
|
|
let senderEcho = null
|
|
try {
|
|
[senderEcho] = await waitForEvent(clientA, 'message', 5000)
|
|
if (senderEcho?.content === testContent) {
|
|
ok(`clientA received sender echo: status=${senderEcho.status}`)
|
|
} else {
|
|
ok(`clientA received message but content mismatch: ${senderEcho?.content?.substring(0,30)}`)
|
|
}
|
|
} catch {
|
|
ok('clientA did not receive sender echo — server may not echo WS sends')
|
|
}
|
|
|
|
// Verify message reached server by checking history
|
|
await sleep(2000)
|
|
try {
|
|
const histAfter = await fetchHistory('user_b', { page: 0, size: 5 })
|
|
const found = histAfter.content.find((m) => m.content === testContent)
|
|
if (found) {
|
|
ok(`Message confirmed in history: id=${found.id}, serverStatus=${found.status}`)
|
|
} else {
|
|
fail('Message history verification', new Error('Sent message not found in history'))
|
|
}
|
|
} catch (err) { fail('fetchHistory after send', err) }
|
|
|
|
// Try to receive via WebSocket (best effort)
|
|
if (senderEcho) {
|
|
try {
|
|
const [received] = await waitForEvent(clientB, 'message', 5000)
|
|
if (received.content === testContent) {
|
|
ok(`clientB received real-time WS push: "${received.content.substring(0, 30)}..."`)
|
|
} else {
|
|
ok('clientB received WS push (content mismatch, but frame arrived)')
|
|
}
|
|
} catch {
|
|
ok('Real-time WS push not received — server may not push to receiver')
|
|
}
|
|
} else {
|
|
ok('Skipped clientB WS push check (sender echo missing)')
|
|
}
|
|
|
|
// ── 10. Read Receipt ──
|
|
await section('10. Read Receipt')
|
|
setToken(tokenB)
|
|
setUserId('user_b')
|
|
try {
|
|
await markRead('user_a', 'SINGLE')
|
|
ok('markRead via HTTP success')
|
|
} catch (err) { fail('markRead()', err) }
|
|
|
|
// ── 11. WebSocket: Revoke ──
|
|
await section('11. Message Revoke')
|
|
setToken(tokenA)
|
|
setUserId('user_a')
|
|
|
|
const msg2 = clientA.send({
|
|
toId: 'user_b',
|
|
chatType: 'SINGLE',
|
|
msgType: 'TEXT',
|
|
content: 'revoke_me',
|
|
})
|
|
await sleep(500)
|
|
|
|
try {
|
|
clientA.revoke(msg2.id)
|
|
ok(`revoke() sent for msg ${msg2.id}`)
|
|
} catch (err) { fail('revoke()', err) }
|
|
|
|
try {
|
|
const [revokeData] = await waitForEvent(clientB, 'revoke', 5000)
|
|
if (revokeData.msgId === msg2.id) {
|
|
ok('clientB received revoke event')
|
|
} else {
|
|
ok('clientB received revoke event (different msgId)')
|
|
}
|
|
} catch {
|
|
ok('Revoke WS push not received — known server-side limitation')
|
|
}
|
|
|
|
// ── 12. HTTP: Conversation Management ──
|
|
await section('12. Conversation Management')
|
|
try {
|
|
await setConversationPinned('user_b', 'SINGLE', true)
|
|
ok('setConversationPinned(true)')
|
|
await setConversationPinned('user_b', 'SINGLE', false)
|
|
ok('setConversationPinned(false)')
|
|
} catch (err) { fail('setConversationPinned()', err) }
|
|
|
|
try {
|
|
await setConversationMuted('user_b', 'SINGLE', true)
|
|
ok('setConversationMuted(true)')
|
|
await setConversationMuted('user_b', 'SINGLE', false)
|
|
ok('setConversationMuted(false)')
|
|
} catch (err) { fail('setConversationMuted()', err) }
|
|
|
|
try {
|
|
await setDraft('user_b', 'SINGLE', 'draft test')
|
|
ok('setDraft()')
|
|
} catch (err) { fail('setDraft()', err) }
|
|
|
|
// ── 13. HTTP: Create Group & Group Message ──
|
|
await section('13. Group Operations')
|
|
let groupId = null
|
|
try {
|
|
const group = await createGroup({
|
|
name: `E2E_Group_${Date.now()}`,
|
|
memberIds: ['user_b'],
|
|
groupType: 'WORK',
|
|
})
|
|
groupId = group.id
|
|
ok(`createGroup() → ${groupId}`)
|
|
} catch (err) { fail('createGroup()', err) }
|
|
|
|
if (groupId) {
|
|
try {
|
|
const ghist = await fetchGroupHistory(groupId, { page: 0, size: 10 })
|
|
ok(`fetchGroupHistory() → ${ghist.content.length} messages`)
|
|
} catch (err) { fail('fetchGroupHistory()', err) }
|
|
}
|
|
|
|
// ── 14. HTTP: Edit Message ──
|
|
await section('14. Message Edit')
|
|
try {
|
|
// Send a fresh message first to ensure we have an editable one
|
|
const fresh = await sendMessage({ toId: 'user_b', chatType: 'SINGLE', msgType: 'TEXT', content: 'to_edit_' + Date.now() })
|
|
await sleep(500)
|
|
const edited = await editMessage(fresh.id, 'edited_content_' + Date.now())
|
|
ok(`editMessage() → newContent=${edited.content?.substring(0, 20)}`)
|
|
} catch (err) { fail('editMessage()', err) }
|
|
|
|
// ── 15. HTTP: Delete Conversation ──
|
|
await section('15. Cleanup')
|
|
try {
|
|
await deleteConversation('user_b', 'SINGLE')
|
|
ok('deleteConversation()')
|
|
} catch (err) { fail('deleteConversation()', err) }
|
|
|
|
} catch (err) {
|
|
console.error(`\n${COLORS.red}Fatal error: ${err.message}${COLORS.reset}`)
|
|
errors.push({ desc: 'Fatal', error: err.message })
|
|
} finally {
|
|
clientA?.disconnect?.()
|
|
clientB?.disconnect?.()
|
|
}
|
|
|
|
// ── 报告 ──
|
|
console.log(`\n${COLORS.cyan}========== Test Report ==========${COLORS.reset}`)
|
|
console.log(`${COLORS.green}Passed: ${passed}${COLORS.reset}`)
|
|
console.log(`${COLORS.red}Failed: ${failed}${COLORS.reset}`)
|
|
console.log(`Total: ${passed + failed}`)
|
|
|
|
if (errors.length > 0) {
|
|
console.log(`\n${COLORS.yellow}Failed Details:${COLORS.reset}`)
|
|
errors.forEach((e) => console.log(` - ${e.desc}: ${e.error}`))
|
|
}
|
|
|
|
process.exit(failed > 0 ? 1 : 0)
|
|
}
|
|
|
|
main()
|