/** * Tests for ChannelConnection * * These tests verify the ChannelConnection class API. * Unit tests for class structure don't require a real backend. * Integration tests for actual WebSocket connections require the backend. * * Backend must be running for integration tests: docker-compose up */ import { ChannelConnection, RPCError } from '../connection' import { describeIntegration, WS_URL } from '../../testing' describe('ChannelConnection (unit tests)', () => { describe('construction', () => { it('should start in disconnected state', () => { const connection = new ChannelConnection({ url: 'ws://localhost/ws/' }) expect(connection.status).toBe('disconnected') }) }) describe('status change handlers', () => { it('should allow subscribing to status changes', () => { const connection = new ChannelConnection({ url: 'ws://localhost/ws/' }) const handler = jest.fn() const unsubscribe = connection.onStatusChange(handler) expect(typeof unsubscribe).toBe('function') }) }) describe('message handlers', () => { it('should allow subscribing to messages', () => { const connection = new ChannelConnection({ url: 'ws://localhost/ws/' }) const handler = jest.fn() const unsubscribe = connection.onMessage(handler) expect(typeof unsubscribe).toBe('function') }) }) describe('send queueing', () => { it('should queue messages when not connected', () => { const connection = new ChannelConnection({ url: 'ws://localhost/ws/', reconnect: false, }) // This shouldn't throw connection.send({ action: 'subscribe', channel: 'test', params: {}, }) // Status should still be disconnected (or connecting if it auto-connected) expect(['disconnected', 'connecting']).toContain(connection.status) }) }) describe('rpc', () => { it('should queue rpc messages when not connected', () => { const connection = new ChannelConnection({ url: 'ws://localhost/ws/', reconnect: false, }) const promise = connection.rpc('test_fn', { arg: 'value' }) expect(promise).toBeInstanceOf(Promise) }) }) }) describeIntegration('ChannelConnection (integration)', () => { describe('real WebSocket connection', () => { it('should connect to real backend WebSocket', async () => { const connection = new ChannelConnection({ url: WS_URL, reconnect: false, }) const statusChanges: string[] = [] connection.onStatusChange((status) => { statusChanges.push(status) }) // Connect connection.connect() // Wait for connection await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Connection timeout')) }, 5000) const unsubscribe = connection.onStatusChange((status) => { if (status === 'connected') { clearTimeout(timeout) unsubscribe() resolve() } }) }) expect(connection.status).toBe('connected') // Cleanup connection.disconnect() }) it('should disconnect cleanly', async () => { const connection = new ChannelConnection({ url: WS_URL, reconnect: false, }) // Connect first connection.connect() await new Promise((resolve) => { const unsubscribe = connection.onStatusChange((status) => { if (status === 'connected') { unsubscribe() resolve() } }) }) // Now disconnect connection.disconnect() // Should be disconnected expect(connection.status).toBe('disconnected') }) }) }) describe('RPCError', () => { it('should be an Error subclass', () => { const error = new RPCError('TEST_CODE', 'Test message') expect(error).toBeInstanceOf(Error) expect(error).toBeInstanceOf(RPCError) }) it('should have correct properties', () => { const error = new RPCError('VALIDATION_ERROR', 'Field is required', { field: 'email' }) expect(error.code).toBe('VALIDATION_ERROR') expect(error.message).toBe('Field is required') expect(error.details).toEqual({ field: 'email' }) expect(error.name).toBe('RPCError') }) it('should work without details', () => { const error = new RPCError('NOT_FOUND', 'Function not found') expect(error.code).toBe('NOT_FOUND') expect(error.message).toBe('Function not found') expect(error.details).toBeUndefined() }) })