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

384 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.
/**
* 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);
});
});
});