348 lines
11 KiB
TypeScript
348 lines
11 KiB
TypeScript
|
|
/**
|
|||
|
|
* 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);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|