Newer
Older
navi-1 / webclient / tests / unit / composables / useWebSocket.test.js
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { useWebSocket } from '@/composables/useWebSocket.js'

class MockWebSocket extends EventTarget {
  constructor(url) {
    super()
    this.url = url
    this.readyState = WebSocket.CONNECTING
    this.sent = []
    MockWebSocket.last = this
  }

  send(data) { this.sent.push(data) }
  close() {
    this.readyState = WebSocket.CLOSED
    this.dispatchEvent(new Event('close'))
  }

  _open() {
    this.readyState = WebSocket.OPEN
    this.dispatchEvent(new Event('open'))
  }

  _msg(data) {
    this.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(data) }))
  }
}

// Stub global WebSocket before each test
beforeEach(() => {
  vi.stubGlobal('WebSocket', MockWebSocket)
  setActivePinia(createPinia())
})

afterEach(() => {
  vi.unstubAllGlobals()
})

describe('useWebSocket', () => {
  it('connect opens socket', () => {
    const ws = useWebSocket()
    ws.connect('s1')
    expect(MockWebSocket.last).toBeTruthy()
    expect(MockWebSocket.last.url).toContain('/ws/sessions/s1')
  })

  it('connected flag after open', () => {
    const ws = useWebSocket()
    ws.connect('s1')
    MockWebSocket.last._open()
    expect(ws.connected.value).toBe(true)
    expect(ws.reconnecting.value).toBe(false)
  })

  it('send delivers JSON when open', () => {
    const ws = useWebSocket()
    ws.connect('s1')
    MockWebSocket.last._open()
    ws.send({ type: 'message', content: 'hi' })
    expect(MockWebSocket.last.sent).toHaveLength(1)
    expect(JSON.parse(MockWebSocket.last.sent[0])).toEqual({ type: 'message', content: 'hi' })
  })

  it('disconnect sets destroyed and clears state', () => {
    const ws = useWebSocket()
    ws.connect('s1')
    MockWebSocket.last._open()
    ws.disconnect()
    expect(ws.connected.value).toBe(false)
  })

  it('dispatches stream_start to chat store', () => {
    const ws = useWebSocket()
    ws.connect('s1')
    MockWebSocket.last._open()
    MockWebSocket.last._msg({ type: 'stream_start' })
    // No crash = dispatched; deeper assertions need chat store spy
    expect(ws.connected.value).toBe(true)
  })

  it('dispatches session_sync', () => {
    const ws = useWebSocket()
    ws.connect('s1')
    MockWebSocket.last._open()
    MockWebSocket.last._msg({ type: 'session_sync' })
    expect(ws.connected.value).toBe(true)
  })

  it('reconnect schedules on close', () => {
    vi.useFakeTimers()
    const ws = useWebSocket()
    ws.connect('s1')
    MockWebSocket.last._open()
    MockWebSocket.last.close()
    expect(ws.reconnecting.value).toBe(true)
    vi.advanceTimersByTime(3000)
    // Should have created a new MockWebSocket instance
    expect(MockWebSocket.last.readyState).toBe(WebSocket.CONNECTING)
    vi.useRealTimers()
  })
})