Files
esengine/packages/network-client/tests/transport/WebSocketClientTransport.test.ts
2025-08-12 09:39:07 +08:00

348 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* WebSocketClientTransport 测试
* 测试WebSocket客户端传输层的构造函数和依赖问题
*/
import { WebSocketClientTransport, WebSocketClientConfig } from '../../src/transport/WebSocketClientTransport';
import { ConnectionState } from '../../src/transport/ClientTransport';
// Mock WebSocket
class MockWebSocket {
public readyState: number = WebSocket.CONNECTING;
public onopen: ((event: Event) => void) | null = null;
public onclose: ((event: CloseEvent) => void) | null = null;
public onmessage: ((event: MessageEvent) => void) | null = null;
public onerror: ((event: Event) => void) | null = null;
constructor(public url: string, public protocols?: string | string[]) {}
send(data: string | ArrayBuffer | Blob): void {}
close(code?: number, reason?: string): void {
this.readyState = WebSocket.CLOSED;
if (this.onclose) {
this.onclose(new CloseEvent('close', { code: code || 1000, reason: reason || '' }));
}
}
}
// Mock依赖 - 直接创建mock对象而不依赖外部模块
const mockCore = {
schedule: {
scheduleRepeating: jest.fn((callback: Function, interval: number) => ({
stop: jest.fn()
}))
}
};
const mockEmitter = {
emit: jest.fn(),
on: jest.fn(),
off: jest.fn(),
removeAllListeners: jest.fn()
};
const mockNetworkShared = {
NetworkValue: {},
generateMessageId: jest.fn(() => 'mock-message-id-123')
};
// 设置模块mock
jest.doMock('@esengine/ecs-framework', () => ({
Core: mockCore,
Emitter: jest.fn(() => mockEmitter)
}));
jest.doMock('@esengine/ecs-framework-network-shared', () => mockNetworkShared);
// 设置全局WebSocket mock
(global as any).WebSocket = MockWebSocket;
(global as any).WebSocket.CONNECTING = 0;
(global as any).WebSocket.OPEN = 1;
(global as any).WebSocket.CLOSING = 2;
(global as any).WebSocket.CLOSED = 3;
describe('WebSocketClientTransport', () => {
let transport: WebSocketClientTransport;
const defaultConfig: WebSocketClientConfig = {
host: 'localhost',
port: 8080,
secure: false,
connectionTimeout: 5000,
reconnectInterval: 1000,
maxReconnectAttempts: 3,
heartbeatInterval: 30000
};
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
if (transport) {
transport.disconnect().catch(() => {});
transport = null as any;
}
});
describe('构造函数测试', () => {
it('应该能够成功创建WebSocketClientTransport实例', () => {
expect(() => {
transport = new WebSocketClientTransport(defaultConfig);
}).not.toThrow();
expect(transport).toBeInstanceOf(WebSocketClientTransport);
});
it('应该正确合并默认配置', () => {
transport = new WebSocketClientTransport(defaultConfig);
const config = (transport as any).config;
expect(config.path).toBe('/ws');
expect(config.protocols).toEqual([]);
expect(config.headers).toEqual({});
expect(config.binaryType).toBe('arraybuffer');
expect(config.host).toBe('localhost');
expect(config.port).toBe(8080);
});
it('应该允许自定义配置覆盖默认值', () => {
const customConfig: WebSocketClientConfig = {
...defaultConfig,
path: '/custom-ws',
protocols: ['custom-protocol'],
headers: { 'X-Custom': 'value' },
binaryType: 'blob'
};
transport = new WebSocketClientTransport(customConfig);
const config = (transport as any).config;
expect(config.path).toBe('/custom-ws');
expect(config.protocols).toEqual(['custom-protocol']);
expect(config.headers).toEqual({ 'X-Custom': 'value' });
expect(config.binaryType).toBe('blob');
});
it('应该正确初始化内部状态', () => {
transport = new WebSocketClientTransport(defaultConfig);
expect((transport as any).websocket).toBeNull();
expect((transport as any).connectionPromise).toBeNull();
expect((transport as any).connectionTimeoutTimer).toBeNull();
expect((transport as any).state).toBe(ConnectionState.DISCONNECTED);
});
});
describe('依赖注入测试', () => {
it('应该正确处理@esengine/ecs-framework依赖', () => {
const { Core } = require('@esengine/ecs-framework');
expect(() => {
transport = new WebSocketClientTransport(defaultConfig);
}).not.toThrow();
expect(Core).toBeDefined();
});
it('应该正确处理@esengine/ecs-framework-network-shared依赖', () => {
const { generateMessageId } = require('@esengine/ecs-framework-network-shared');
expect(() => {
transport = new WebSocketClientTransport(defaultConfig);
}).not.toThrow();
expect(generateMessageId).toBeDefined();
expect(typeof generateMessageId).toBe('function');
});
});
describe('连接功能测试', () => {
beforeEach(() => {
transport = new WebSocketClientTransport(defaultConfig);
});
it('应该能够发起连接', async () => {
const connectPromise = transport.connect();
expect((transport as any).websocket).toBeInstanceOf(MockWebSocket);
expect((transport as any).state).toBe(ConnectionState.CONNECTING);
// 模拟连接成功
const ws = (transport as any).websocket as MockWebSocket;
ws.readyState = WebSocket.OPEN;
if (ws.onopen) {
ws.onopen(new Event('open'));
}
await expect(connectPromise).resolves.toBeUndefined();
});
it('应该构造正确的WebSocket URL', async () => {
transport.connect();
const ws = (transport as any).websocket as MockWebSocket;
expect(ws.url).toBe('ws://localhost:8080/ws');
});
it('使用安全连接时应该构造HTTPS URL', async () => {
const secureConfig = { ...defaultConfig, secure: true };
transport = new WebSocketClientTransport(secureConfig);
transport.connect();
const ws = (transport as any).websocket as MockWebSocket;
expect(ws.url).toBe('wss://localhost:8080/ws');
});
it('应该设置WebSocket事件处理器', async () => {
transport.connect();
const ws = (transport as any).websocket as MockWebSocket;
expect(ws.onopen).toBeDefined();
expect(ws.onclose).toBeDefined();
expect(ws.onmessage).toBeDefined();
expect(ws.onerror).toBeDefined();
});
it('连接超时应该被正确处理', async () => {
const shortTimeoutConfig = { ...defaultConfig, connectionTimeout: 100 };
transport = new WebSocketClientTransport(shortTimeoutConfig);
const connectPromise = transport.connect();
// 不触发onopen事件让连接超时
await expect(connectPromise).rejects.toThrow('连接超时');
});
it('应该能够正确断开连接', async () => {
transport.connect();
// 模拟连接成功
const ws = (transport as any).websocket as MockWebSocket;
ws.readyState = WebSocket.OPEN;
if (ws.onopen) {
ws.onopen(new Event('open'));
}
await transport.disconnect();
expect((transport as any).state).toBe(ConnectionState.DISCONNECTED);
});
});
describe('消息发送测试', () => {
beforeEach(async () => {
transport = new WebSocketClientTransport(defaultConfig);
});
it('未连接时发送消息应该加入队列', async () => {
const message = {
type: 'custom' as const,
data: { test: 'data' },
reliable: true,
timestamp: Date.now()
};
await transport.sendMessage(message);
const messageQueue = (transport as any).messageQueue;
expect(messageQueue).toHaveLength(1);
expect(messageQueue[0]).toEqual(message);
});
it('连接后应该发送队列中的消息', async () => {
const message = {
type: 'custom' as const,
data: { test: 'data' },
reliable: true,
timestamp: Date.now()
};
// 先发送消息到队列
await transport.sendMessage(message);
// 然后连接
transport.connect();
const ws = (transport as any).websocket as MockWebSocket;
const sendSpy = jest.spyOn(ws, 'send');
// 模拟连接成功
ws.readyState = WebSocket.OPEN;
if (ws.onopen) {
ws.onopen(new Event('open'));
}
expect(sendSpy).toHaveBeenCalled();
expect((transport as any).messageQueue).toHaveLength(0);
});
});
describe('错误处理测试', () => {
it('应该处理WebSocket构造函数异常', () => {
// Mock WebSocket构造函数抛出异常
const originalWebSocket = (global as any).WebSocket;
(global as any).WebSocket = jest.fn(() => {
throw new Error('WebSocket构造失败');
});
transport = new WebSocketClientTransport(defaultConfig);
expect(transport.connect()).rejects.toThrow('WebSocket构造失败');
// 恢复原始WebSocket
(global as any).WebSocket = originalWebSocket;
});
it('应该处理网络连接错误', async () => {
transport = new WebSocketClientTransport(defaultConfig);
const connectPromise = transport.connect();
// 模拟连接错误
const ws = (transport as any).websocket as MockWebSocket;
if (ws.onerror) {
ws.onerror(new Event('error'));
}
await expect(connectPromise).rejects.toThrow();
});
it('应该处理意外的连接关闭', () => {
transport = new WebSocketClientTransport(defaultConfig);
transport.connect();
const ws = (transport as any).websocket as MockWebSocket;
// 模拟连接意外关闭
if (ws.onclose) {
ws.onclose(new CloseEvent('close', { code: 1006, reason: '意外关闭' }));
}
expect((transport as any).state).toBe(ConnectionState.DISCONNECTED);
});
});
describe('统计信息测试', () => {
it('应该正确计算连接统计信息', async () => {
transport = new WebSocketClientTransport(defaultConfig);
const initialStats = transport.getStats();
expect(initialStats.connectedAt).toBeNull();
expect(initialStats.messagesSent).toBe(0);
expect(initialStats.messagesReceived).toBe(0);
});
it('连接后应该更新统计信息', async () => {
transport = new WebSocketClientTransport(defaultConfig);
transport.connect();
const ws = (transport as any).websocket as MockWebSocket;
ws.readyState = WebSocket.OPEN;
if (ws.onopen) {
ws.onopen(new Event('open'));
}
const stats = transport.getStats();
expect(stats.connectedAt).toBeInstanceOf(Date);
});
});
});