XuqmGroup-Vue3SDK/__tests__/unit/im/useIm.test.ts
2026-05-07 19:39:45 +08:00

247 行
6.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { nextTick } from 'vue'
import { useIm } from '../../../src/im/useIm'
import { init, setToken, setUserId } from '../../../src/core/sdk'
class MockWebSocket {
static OPEN = 1
static CONNECTING = 0
static CLOSING = 2
static CLOSED = 3
static instances: MockWebSocket[] = []
url = ''
readyState = 0
onopen: ((ev: Event) => void) | null = null
onmessage: ((ev: MessageEvent) => void) | null = null
onclose: ((ev: CloseEvent) => void) | null = null
onerror: ((ev: Event) => void) | null = null
sent: string[] = []
constructor(url: string) {
this.url = url
MockWebSocket.instances.push(this)
}
send(data: string) {
this.sent.push(data)
if (data.startsWith('CONNECT')) {
setTimeout(() => {
this.onmessage?.(
new MessageEvent('message', {
data: 'CONNECTED\nversion:1.2\nheart-beat:0,0\nuser-name:test_user\n\n\x00',
})
)
}, 10)
}
}
close() {
this.readyState = 3
this.onclose?.(new CloseEvent('close', { code: 1000, reason: 'test' }))
}
}
describe('im/useIm', () => {
let originalWebSocket: typeof WebSocket
let composable: ReturnType<typeof useIm>
beforeEach(() => {
originalWebSocket = globalThis.WebSocket
globalThis.WebSocket = MockWebSocket as unknown as typeof WebSocket
MockWebSocket.instances = []
init({ appKey: 'ak_demo_chat', appSecret: 'secret', debug: false, baseUrl: 'http://test', wsUrl: 'ws://test/ws' })
setToken('test_token')
setUserId('user_a')
vi.useFakeTimers({ shouldAdvanceTime: true })
composable = useIm()
})
afterEach(() => {
composable.disconnect()
globalThis.WebSocket = originalWebSocket
vi.useRealTimers()
})
function openWs(index = 0) {
const ws = MockWebSocket.instances[index]
ws.readyState = 1
ws.onopen?.(new Event('open'))
}
function emitStompMessage(ws: MockWebSocket, payload: object) {
ws.onmessage?.(
new MessageEvent('message', {
data: `MESSAGE\ndestination:/user/queue/messages\n\n${JSON.stringify(payload)}\x00`,
})
)
}
it('should initialize with disconnected state', () => {
expect(composable.connected.value).toBe(false)
expect(composable.messages.value).toEqual([])
expect(composable.conversations.value).toEqual([])
expect(composable.error.value).toBeNull()
})
it('should set connected to true after connect', async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
json: vi.fn().mockResolvedValue({ code: 200, data: [], message: 'ok' }),
} as unknown as Response)
composable.connect()
openWs()
await vi.advanceTimersByTimeAsync(20)
await nextTick()
expect(composable.connected.value).toBe(true)
})
it('should add message to messages list on send', async () => {
composable.connect()
openWs()
await vi.advanceTimersByTimeAsync(20)
const msg = composable.send({
toId: 'user_b',
chatType: 'SINGLE',
msgType: 'TEXT',
content: 'hello',
})
await nextTick()
expect(composable.messages.value).toHaveLength(1)
expect(composable.messages.value[0].content).toBe('hello')
expect(composable.messages.value[0].id).toBe(msg.id)
})
it('should upsert message when receiving same id', async () => {
composable.connect()
openWs()
await vi.advanceTimersByTimeAsync(20)
const msg = composable.send({
toId: 'user_b',
chatType: 'SINGLE',
msgType: 'TEXT',
content: 'hello',
})
await nextTick()
const ws = MockWebSocket.instances[0]
emitStompMessage(ws, {
id: msg.id,
appKey: 'ak_demo_chat',
fromUserId: 'user_b',
toId: 'user_a',
chatType: 'SINGLE',
msgType: 'TEXT',
content: 'updated',
status: 'READ',
createdAt: new Date().toISOString(),
})
await nextTick()
expect(composable.messages.value).toHaveLength(1)
expect(composable.messages.value[0].content).toBe('updated')
expect(composable.messages.value[0].status).toBe('READ')
})
it('should mark message as revoked', async () => {
composable.connect()
openWs()
await vi.advanceTimersByTimeAsync(20)
const msg = composable.send({
toId: 'user_b',
chatType: 'SINGLE',
msgType: 'TEXT',
content: 'hello',
})
await nextTick()
const ws = MockWebSocket.instances[0]
emitStompMessage(ws, {
id: msg.id,
appKey: 'ak_demo_chat',
fromUserId: 'user_b',
toId: 'user_a',
chatType: 'SINGLE',
msgType: 'REVOKED',
content: '',
status: 'REVOKED',
createdAt: new Date().toISOString(),
})
await nextTick()
expect(composable.messages.value[0].status).toBe('REVOKED')
expect(composable.messages.value[0].msgType).toBe('REVOKED')
expect(composable.messages.value[0].revoked).toBe(true)
expect(composable.messages.value[0].content).toBe('')
})
it('should mark conversation read locally', async () => {
composable.conversations.value = [
{
targetId: 'user_b',
chatType: 'SINGLE',
lastMsgTime: Date.now(),
unreadCount: 5,
isMuted: false,
isPinned: false,
},
]
composable.setConversationRead('user_b', 'SINGLE')
await nextTick()
expect(composable.conversations.value[0].unreadCount).toBe(0)
})
it('should sort conversations by pinned then lastMsgTime', async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
json: vi.fn().mockResolvedValue({
code: 200,
data: [
{ targetId: 'a', chatType: 'SINGLE', lastMsgTime: 1000, unreadCount: 0, isMuted: false, isPinned: false },
{ targetId: 'b', chatType: 'SINGLE', lastMsgTime: 500, unreadCount: 0, isMuted: false, isPinned: true },
{ targetId: 'c', chatType: 'SINGLE', lastMsgTime: 2000, unreadCount: 0, isMuted: false, isPinned: false },
],
message: 'ok',
}),
} as unknown as Response)
composable.connect()
openWs()
await vi.advanceTimersByTimeAsync(20)
await nextTick()
expect(composable.conversations.value[0].targetId).toBe('b') // pinned first
expect(composable.conversations.value[1].targetId).toBe('c') // newer
expect(composable.conversations.value[2].targetId).toBe('a') // older
})
it('should throw when sending without connection', () => {
expect(() =>
composable.send({
toId: 'user_b',
chatType: 'SINGLE',
msgType: 'TEXT',
content: 'hello',
})
).toThrow('IM client not connected')
})
it('should disconnect and reset state', async () => {
composable.connect()
openWs()
await vi.advanceTimersByTimeAsync(20)
await nextTick()
expect(composable.connected.value).toBe(true)
composable.disconnect()
await nextTick()
expect(composable.connected.value).toBe(false)
expect(composable.messages.value).toEqual([])
})
})