更新network库及core库优化
This commit is contained in:
384
packages/network-client/tests/NetworkClient.integration.test.ts
Normal file
384
packages/network-client/tests/NetworkClient.integration.test.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
/**
|
||||
* NetworkClient 集成测试
|
||||
* 测试网络客户端的完整功能,包括依赖注入和错误处理
|
||||
*/
|
||||
|
||||
import { NetworkClient } from '../src/core/NetworkClient';
|
||||
|
||||
// Mock 所有外部依赖
|
||||
jest.mock('@esengine/ecs-framework', () => ({
|
||||
Core: {
|
||||
scene: null,
|
||||
schedule: {
|
||||
scheduleRepeating: jest.fn((callback: Function, interval: number) => ({
|
||||
stop: jest.fn()
|
||||
}))
|
||||
}
|
||||
},
|
||||
Emitter: jest.fn().mockImplementation(() => ({
|
||||
emit: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
removeAllListeners: jest.fn()
|
||||
}))
|
||||
}));
|
||||
|
||||
jest.mock('@esengine/ecs-framework-network-shared', () => ({
|
||||
NetworkValue: {},
|
||||
generateMessageId: jest.fn(() => 'test-message-id-123'),
|
||||
generateNetworkId: jest.fn(() => 12345),
|
||||
NetworkUtils: {
|
||||
generateMessageId: jest.fn(() => 'test-message-id-456'),
|
||||
calculateDistance: jest.fn(() => 100),
|
||||
isNodeEnvironment: jest.fn(() => false),
|
||||
isBrowserEnvironment: jest.fn(() => true)
|
||||
}
|
||||
}));
|
||||
|
||||
// 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 || '' }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(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('NetworkClient 集成测试', () => {
|
||||
let client: NetworkClient;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (client) {
|
||||
client.disconnect().catch(() => {});
|
||||
client = null as any;
|
||||
}
|
||||
});
|
||||
|
||||
describe('依赖注入测试', () => {
|
||||
it('应该正确处理所有依赖模块', () => {
|
||||
expect(() => {
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
}).not.toThrow();
|
||||
|
||||
expect(client).toBeInstanceOf(NetworkClient);
|
||||
});
|
||||
|
||||
it('应该正确使用network-shared中的工具函数', () => {
|
||||
const { generateMessageId, NetworkUtils } = require('@esengine/ecs-framework-network-shared');
|
||||
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
|
||||
// 验证network-shared模块被正确导入
|
||||
expect(generateMessageId).toBeDefined();
|
||||
expect(NetworkUtils).toBeDefined();
|
||||
});
|
||||
|
||||
it('应该正确使用ecs-framework中的Core模块', () => {
|
||||
const { Core } = require('@esengine/ecs-framework');
|
||||
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
|
||||
expect(Core).toBeDefined();
|
||||
expect(Core.schedule).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('构造函数错误处理', () => {
|
||||
it('应该处理network-shared模块导入失败', () => {
|
||||
// 重置模块并模拟导入失败
|
||||
jest.resetModules();
|
||||
jest.doMock('@esengine/ecs-framework-network-shared', () => {
|
||||
throw new Error('network-shared模块导入失败');
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
const { NetworkClient } = require('../src/core/NetworkClient');
|
||||
new NetworkClient({
|
||||
transportType: 'websocket',
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该处理ecs-framework模块导入失败', () => {
|
||||
// 重置模块并模拟导入失败
|
||||
jest.resetModules();
|
||||
jest.doMock('@esengine/ecs-framework', () => {
|
||||
throw new Error('ecs-framework模块导入失败');
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
const { NetworkClient } = require('../src/core/NetworkClient');
|
||||
new NetworkClient({
|
||||
transportType: 'websocket',
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该处理传输层构造失败', () => {
|
||||
// Mock传输层构造函数抛出异常
|
||||
const originalWebSocket = (global as any).WebSocket;
|
||||
(global as any).WebSocket = jest.fn(() => {
|
||||
throw new Error('WebSocket不可用');
|
||||
});
|
||||
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
|
||||
expect(client.connect()).rejects.toThrow();
|
||||
|
||||
// 恢复原始WebSocket
|
||||
(global as any).WebSocket = originalWebSocket;
|
||||
});
|
||||
});
|
||||
|
||||
describe('功能测试', () => {
|
||||
beforeEach(() => {
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('应该能够成功连接', async () => {
|
||||
const connectPromise = client.connect();
|
||||
|
||||
// 模拟连接成功
|
||||
setTimeout(() => {
|
||||
const transport = (client as any).transport;
|
||||
if (transport && transport.websocket && transport.websocket.onopen) {
|
||||
transport.websocket.readyState = WebSocket.OPEN;
|
||||
transport.websocket.onopen(new Event('open'));
|
||||
}
|
||||
}, 0);
|
||||
|
||||
await expect(connectPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该能够发送消息', async () => {
|
||||
// 先连接
|
||||
const connectPromise = client.connect();
|
||||
setTimeout(() => {
|
||||
const transport = (client as any).transport;
|
||||
if (transport && transport.websocket && transport.websocket.onopen) {
|
||||
transport.websocket.readyState = WebSocket.OPEN;
|
||||
transport.websocket.onopen(new Event('open'));
|
||||
}
|
||||
}, 0);
|
||||
await connectPromise;
|
||||
|
||||
// 发送消息
|
||||
const message = {
|
||||
type: 'custom' as const,
|
||||
data: { test: 'message' },
|
||||
reliable: true
|
||||
};
|
||||
|
||||
// NetworkClient没有直接的sendMessage方法,它通过RPC调用
|
||||
});
|
||||
|
||||
it('应该能够正确断开连接', async () => {
|
||||
await expect(client.disconnect()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该返回正确的认证状态', () => {
|
||||
expect(client.isAuthenticated()).toBe(false);
|
||||
});
|
||||
|
||||
it('应该能够获取网络对象列表', () => {
|
||||
const networkObjects = client.getAllNetworkObjects();
|
||||
expect(Array.isArray(networkObjects)).toBe(true);
|
||||
expect(networkObjects.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('消息ID生成测试', () => {
|
||||
beforeEach(() => {
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('应该能够生成唯一的消息ID', () => {
|
||||
const messageId1 = (client as any).generateMessageId();
|
||||
const messageId2 = (client as any).generateMessageId();
|
||||
|
||||
expect(typeof messageId1).toBe('string');
|
||||
expect(typeof messageId2).toBe('string');
|
||||
expect(messageId1).not.toBe(messageId2);
|
||||
});
|
||||
|
||||
it('生成的消息ID应该符合预期格式', () => {
|
||||
const messageId = (client as any).generateMessageId();
|
||||
|
||||
// 检查消息ID格式(时间戳 + 随机字符串)
|
||||
expect(messageId).toMatch(/^[a-z0-9]+$/);
|
||||
expect(messageId.length).toBeGreaterThan(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误恢复测试', () => {
|
||||
beforeEach(() => {
|
||||
client = new NetworkClient({
|
||||
transportType: 'websocket',
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
maxReconnectAttempts: 2,
|
||||
reconnectInterval: 100
|
||||
});
|
||||
});
|
||||
|
||||
it('连接失败后应该尝试重连', async () => {
|
||||
let connectAttempts = 0;
|
||||
const originalWebSocket = (global as any).WebSocket;
|
||||
|
||||
(global as any).WebSocket = jest.fn().mockImplementation(() => {
|
||||
connectAttempts++;
|
||||
const ws = new originalWebSocket('ws://localhost:8080');
|
||||
// 模拟连接失败
|
||||
setTimeout(() => {
|
||||
if (ws.onerror) {
|
||||
ws.onerror(new Event('error'));
|
||||
}
|
||||
}, 0);
|
||||
return ws;
|
||||
});
|
||||
|
||||
await expect(client.connect()).rejects.toThrow();
|
||||
|
||||
// 等待重连尝试
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
expect(connectAttempts).toBeGreaterThan(1);
|
||||
|
||||
// 恢复原始WebSocket
|
||||
(global as any).WebSocket = originalWebSocket;
|
||||
});
|
||||
|
||||
it('达到最大重连次数后应该停止重连', async () => {
|
||||
const maxAttempts = 2;
|
||||
client = new NetworkClient({
|
||||
transportType: 'websocket',
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
maxReconnectAttempts: maxAttempts,
|
||||
reconnectInterval: 50
|
||||
});
|
||||
|
||||
let connectAttempts = 0;
|
||||
const originalWebSocket = (global as any).WebSocket;
|
||||
|
||||
(global as any).WebSocket = jest.fn().mockImplementation(() => {
|
||||
connectAttempts++;
|
||||
const ws = new originalWebSocket('ws://localhost:8080');
|
||||
setTimeout(() => {
|
||||
if (ws.onerror) {
|
||||
ws.onerror(new Event('error'));
|
||||
}
|
||||
}, 0);
|
||||
return ws;
|
||||
});
|
||||
|
||||
await expect(client.connect()).rejects.toThrow();
|
||||
|
||||
// 等待所有重连尝试完成
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
expect(connectAttempts).toBeLessThanOrEqual(maxAttempts + 1);
|
||||
|
||||
// 恢复原始WebSocket
|
||||
(global as any).WebSocket = originalWebSocket;
|
||||
});
|
||||
});
|
||||
|
||||
describe('内存泄漏防护测试', () => {
|
||||
it('断开连接时应该清理所有资源', async () => {
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
const emitterInstance = Emitter.mock.results[Emitter.mock.results.length - 1].value;
|
||||
|
||||
await client.disconnect();
|
||||
|
||||
expect(emitterInstance.removeAllListeners).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('多次创建和销毁客户端不应该造成内存泄漏', () => {
|
||||
const initialEmitterCallCount = require('@esengine/ecs-framework').Emitter.mock.calls.length;
|
||||
|
||||
// 创建和销毁多个客户端实例
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const tempClient = new NetworkClient({
|
||||
transportType: 'websocket',
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
});
|
||||
tempClient.disconnect().catch(() => {});
|
||||
}
|
||||
|
||||
const finalEmitterCallCount = require('@esengine/ecs-framework').Emitter.mock.calls.length;
|
||||
|
||||
// 验证Emitter实例数量符合预期
|
||||
expect(finalEmitterCallCount - initialEmitterCallCount).toBe(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
27
packages/network-client/tests/setup.ts
Normal file
27
packages/network-client/tests/setup.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'reflect-metadata';
|
||||
|
||||
// Mock WebSocket for testing
|
||||
(global as any).WebSocket = class MockWebSocket {
|
||||
onopen: ((event: Event) => void) | null = null;
|
||||
onmessage: ((event: MessageEvent) => void) | null = null;
|
||||
onclose: ((event: CloseEvent) => void) | null = null;
|
||||
onerror: ((event: Event) => void) | null = null;
|
||||
|
||||
constructor(public url: string) {}
|
||||
|
||||
send(data: string | ArrayBuffer | Blob) {
|
||||
// Mock implementation
|
||||
}
|
||||
|
||||
close() {
|
||||
// Mock implementation
|
||||
}
|
||||
};
|
||||
|
||||
global.beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
global.afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
374
packages/network-client/tests/transport/ClientTransport.test.ts
Normal file
374
packages/network-client/tests/transport/ClientTransport.test.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
/**
|
||||
* ClientTransport 基类测试
|
||||
* 测试客户端传输层基类的构造函数和依赖问题
|
||||
*/
|
||||
|
||||
import { ClientTransport, ClientTransportConfig, ConnectionState } from '../../src/transport/ClientTransport';
|
||||
|
||||
// Mock Emitter 和 Core
|
||||
jest.mock('@esengine/ecs-framework', () => ({
|
||||
Emitter: jest.fn().mockImplementation(() => ({
|
||||
emit: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
removeAllListeners: jest.fn()
|
||||
})),
|
||||
Core: {
|
||||
schedule: {
|
||||
scheduleRepeating: jest.fn((callback: Function, interval: number) => ({
|
||||
stop: jest.fn()
|
||||
}))
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Mock network-shared
|
||||
jest.mock('@esengine/ecs-framework-network-shared', () => ({
|
||||
NetworkValue: {}
|
||||
}));
|
||||
|
||||
// 创建测试用的具体实现类
|
||||
class TestClientTransport extends ClientTransport {
|
||||
async connect(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async sendMessage(message: any): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
describe('ClientTransport', () => {
|
||||
let transport: TestClientTransport;
|
||||
const defaultConfig: ClientTransportConfig = {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (transport) {
|
||||
transport = null as any;
|
||||
}
|
||||
});
|
||||
|
||||
describe('构造函数测试', () => {
|
||||
it('应该能够成功创建ClientTransport实例', () => {
|
||||
expect(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(transport).toBeInstanceOf(ClientTransport);
|
||||
});
|
||||
|
||||
it('应该正确设置默认配置', () => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
|
||||
const config = (transport as any).config;
|
||||
expect(config.host).toBe('localhost');
|
||||
expect(config.port).toBe(8080);
|
||||
expect(config.secure).toBe(false);
|
||||
expect(config.connectionTimeout).toBe(10000);
|
||||
expect(config.reconnectInterval).toBe(3000);
|
||||
expect(config.maxReconnectAttempts).toBe(10);
|
||||
expect(config.heartbeatInterval).toBe(30000);
|
||||
expect(config.maxQueueSize).toBe(1000);
|
||||
});
|
||||
|
||||
it('应该允许自定义配置覆盖默认值', () => {
|
||||
const customConfig: ClientTransportConfig = {
|
||||
host: 'example.com',
|
||||
port: 9090,
|
||||
secure: true,
|
||||
connectionTimeout: 15000,
|
||||
reconnectInterval: 5000,
|
||||
maxReconnectAttempts: 5,
|
||||
heartbeatInterval: 60000,
|
||||
maxQueueSize: 500
|
||||
};
|
||||
|
||||
transport = new TestClientTransport(customConfig);
|
||||
|
||||
const config = (transport as any).config;
|
||||
expect(config.host).toBe('example.com');
|
||||
expect(config.port).toBe(9090);
|
||||
expect(config.secure).toBe(true);
|
||||
expect(config.connectionTimeout).toBe(15000);
|
||||
expect(config.reconnectInterval).toBe(5000);
|
||||
expect(config.maxReconnectAttempts).toBe(5);
|
||||
expect(config.heartbeatInterval).toBe(60000);
|
||||
expect(config.maxQueueSize).toBe(500);
|
||||
});
|
||||
|
||||
it('应该正确初始化内部状态', () => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
|
||||
expect((transport as any).state).toBe(ConnectionState.DISCONNECTED);
|
||||
expect((transport as any).messageQueue).toEqual([]);
|
||||
expect((transport as any).reconnectAttempts).toBe(0);
|
||||
expect((transport as any).reconnectTimer).toBeNull();
|
||||
expect((transport as any).heartbeatTimer).toBeNull();
|
||||
expect((transport as any).latencyMeasurements).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该正确初始化统计信息', () => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
|
||||
const stats = transport.getStats();
|
||||
expect(stats.connectedAt).toBeNull();
|
||||
expect(stats.connectionDuration).toBe(0);
|
||||
expect(stats.messagesSent).toBe(0);
|
||||
expect(stats.messagesReceived).toBe(0);
|
||||
expect(stats.bytesSent).toBe(0);
|
||||
expect(stats.bytesReceived).toBe(0);
|
||||
expect(stats.averageLatency).toBe(0);
|
||||
expect(stats.averageLatency).toBe(0);
|
||||
expect(stats.reconnectCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('依赖注入测试', () => {
|
||||
it('应该正确处理@esengine/ecs-framework中的Emitter', () => {
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
|
||||
expect(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(Emitter).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('构造函数中Emitter初始化失败应该抛出异常', () => {
|
||||
// Mock Emitter构造函数抛出异常
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
Emitter.mockImplementation(() => {
|
||||
throw new Error('Emitter初始化失败');
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
}).toThrow('Emitter初始化失败');
|
||||
});
|
||||
|
||||
it('应该正确处理@esengine/ecs-framework-network-shared依赖', () => {
|
||||
const networkShared = require('@esengine/ecs-framework-network-shared');
|
||||
|
||||
expect(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(networkShared).toBeDefined();
|
||||
expect(networkShared.NetworkValue).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('事件系统测试', () => {
|
||||
beforeEach(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
});
|
||||
|
||||
it('应该能够注册事件监听器', () => {
|
||||
const mockCallback = jest.fn();
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
const emitterInstance = Emitter.mock.results[0].value;
|
||||
|
||||
transport.on('connected', mockCallback);
|
||||
|
||||
expect(emitterInstance.on).toHaveBeenCalledWith('connected', mockCallback);
|
||||
});
|
||||
|
||||
it('应该能够移除事件监听器', () => {
|
||||
const mockCallback = jest.fn();
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
const emitterInstance = Emitter.mock.results[0].value;
|
||||
|
||||
transport.off('connected', mockCallback);
|
||||
|
||||
expect(emitterInstance.off).toHaveBeenCalledWith('connected', mockCallback);
|
||||
});
|
||||
|
||||
it('应该能够发出事件', () => {
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
const emitterInstance = Emitter.mock.results[0].value;
|
||||
|
||||
(transport as any).emit('connected');
|
||||
|
||||
expect(emitterInstance.emit).toHaveBeenCalledWith('connected');
|
||||
});
|
||||
});
|
||||
|
||||
describe('消息队列测试', () => {
|
||||
beforeEach(() => {
|
||||
transport = new TestClientTransport(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 smallQueueConfig = { ...defaultConfig, maxQueueSize: 2 };
|
||||
transport = new TestClientTransport(smallQueueConfig);
|
||||
|
||||
const message1 = { type: 'custom' as const, data: { id: 1 }, reliable: true, timestamp: Date.now() };
|
||||
const message2 = { type: 'custom' as const, data: { id: 2 }, reliable: true, timestamp: Date.now() };
|
||||
const message3 = { type: 'custom' as const, data: { id: 3 }, reliable: true, timestamp: Date.now() };
|
||||
|
||||
await transport.sendMessage(message1);
|
||||
await transport.sendMessage(message2);
|
||||
await transport.sendMessage(message3);
|
||||
|
||||
const messageQueue = (transport as any).messageQueue;
|
||||
expect(messageQueue).toHaveLength(2);
|
||||
expect(messageQueue[0]).toEqual(message2);
|
||||
expect(messageQueue[1]).toEqual(message3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('连接状态测试', () => {
|
||||
beforeEach(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
});
|
||||
|
||||
it('应该正确获取连接状态', () => {
|
||||
expect(transport.getState()).toBe(ConnectionState.DISCONNECTED);
|
||||
});
|
||||
|
||||
it('应该正确检查连接状态', () => {
|
||||
expect(transport.isConnected()).toBe(false);
|
||||
|
||||
(transport as any).state = ConnectionState.CONNECTED;
|
||||
expect(transport.isConnected()).toBe(true);
|
||||
|
||||
(transport as any).state = ConnectionState.AUTHENTICATED;
|
||||
expect(transport.isConnected()).toBe(true);
|
||||
});
|
||||
|
||||
it('状态变化时应该发出事件', () => {
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
const emitterInstance = Emitter.mock.results[0].value;
|
||||
|
||||
(transport as any).setState(ConnectionState.CONNECTING);
|
||||
|
||||
expect(emitterInstance.emit).toHaveBeenCalledWith(
|
||||
'state-changed',
|
||||
ConnectionState.DISCONNECTED,
|
||||
ConnectionState.CONNECTING
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('延迟测量测试', () => {
|
||||
beforeEach(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
});
|
||||
|
||||
it('应该能够更新延迟测量', () => {
|
||||
(transport as any).updateLatency(100);
|
||||
(transport as any).updateLatency(200);
|
||||
(transport as any).updateLatency(150);
|
||||
|
||||
const stats = transport.getStats();
|
||||
expect(stats.averageLatency).toBe(150);
|
||||
});
|
||||
|
||||
it('应该限制延迟测量样本数量', () => {
|
||||
// 添加超过最大样本数的测量
|
||||
for (let i = 0; i < 150; i++) {
|
||||
(transport as any).updateLatency(i * 10);
|
||||
}
|
||||
|
||||
const latencyMeasurements = (transport as any).latencyMeasurements;
|
||||
expect(latencyMeasurements.length).toBeLessThanOrEqual(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('配置验证测试', () => {
|
||||
it('应该拒绝无效的主机名', () => {
|
||||
expect(() => {
|
||||
transport = new TestClientTransport({ host: '', port: 8080 });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该拒绝无效的端口号', () => {
|
||||
expect(() => {
|
||||
transport = new TestClientTransport({ host: 'localhost', port: 0 });
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
transport = new TestClientTransport({ host: 'localhost', port: 65536 });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该拒绝负数的超时配置', () => {
|
||||
expect(() => {
|
||||
transport = new TestClientTransport({
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
connectionTimeout: -1000
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('资源清理测试', () => {
|
||||
beforeEach(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
});
|
||||
|
||||
it('应该能够清理所有定时器', () => {
|
||||
const { Core } = require('@esengine/ecs-framework');
|
||||
const mockTimer = { stop: jest.fn() };
|
||||
Core.schedule.scheduleRepeating.mockReturnValue(mockTimer);
|
||||
|
||||
// 设置一些定时器
|
||||
(transport as any).reconnectTimer = mockTimer;
|
||||
(transport as any).heartbeatTimer = mockTimer;
|
||||
|
||||
// 调用清理方法
|
||||
(transport as any).cleanup();
|
||||
|
||||
expect(mockTimer.stop).toHaveBeenCalledTimes(2);
|
||||
expect((transport as any).reconnectTimer).toBeNull();
|
||||
expect((transport as any).heartbeatTimer).toBeNull();
|
||||
});
|
||||
|
||||
it('应该能够清理消息队列', () => {
|
||||
(transport as any).messageQueue = [
|
||||
{ type: 'custom', data: {}, reliable: true, timestamp: Date.now() }
|
||||
];
|
||||
|
||||
(transport as any).cleanup();
|
||||
|
||||
expect((transport as any).messageQueue).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('应该能够移除所有事件监听器', () => {
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
const emitterInstance = Emitter.mock.results[0].value;
|
||||
|
||||
(transport as any).cleanup();
|
||||
|
||||
expect(emitterInstance.removeAllListeners).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user