Files
esengine/tests/Utils/Serialization/ProtobufDecorators.test.ts
2025-08-06 17:04:02 +08:00

278 lines
9.4 KiB
TypeScript

/**
* Protobuf装饰器测试
*/
import { Component } from '../../../src/ECS/Component';
import {
ProtoSerializable,
ProtoField,
ProtoFieldType,
ProtoFloat,
ProtoInt32,
ProtoString,
ProtoBool,
ProtobufRegistry,
isProtoSerializable,
getProtoName
} from '../../../src/Utils/Serialization/ProtobufDecorators';
// 测试组件
@ProtoSerializable('TestPosition')
class TestPositionComponent extends Component {
@ProtoFloat(1)
public x: number = 0;
@ProtoFloat(2)
public y: number = 0;
@ProtoFloat(3)
public z: number = 0;
constructor(x: number = 0, y: number = 0, z: number = 0) {
super();
this.x = x;
this.y = y;
this.z = z;
}
}
@ProtoSerializable('TestPlayer')
class TestPlayerComponent extends Component {
@ProtoString(1)
public name: string = '';
@ProtoInt32(2)
public level: number = 1;
@ProtoInt32(3)
public health: number = 100;
@ProtoBool(4)
public isAlive: boolean = true;
constructor(name: string = '', level: number = 1) {
super();
this.name = name;
this.level = level;
}
}
// 没有装饰器的组件
class PlainComponent extends Component {
public data: string = 'test';
}
// 测试字段编号冲突的组件
const createConflictingComponent = () => {
try {
@ProtoSerializable('Conflict')
class ConflictComponent extends Component {
@ProtoFloat(1)
public x: number = 0;
@ProtoFloat(1) // 故意使用相同的字段编号
public y: number = 0;
}
return ConflictComponent;
} catch (error) {
return error;
}
};
describe('ProtobufDecorators', () => {
let registry: ProtobufRegistry;
beforeEach(() => {
// 获取注册表实例
registry = ProtobufRegistry.getInstance();
});
describe('@ProtoSerializable装饰器', () => {
it('应该正确标记组件为可序列化', () => {
const component = new TestPositionComponent(10, 20, 30);
expect(isProtoSerializable(component)).toBe(true);
expect(getProtoName(component)).toBe('TestPosition');
});
it('应该在注册表中注册组件定义', () => {
expect(registry.hasProtoDefinition('TestPosition')).toBe(true);
expect(registry.hasProtoDefinition('TestPlayer')).toBe(true);
});
it('应该正确处理没有装饰器的组件', () => {
const component = new PlainComponent();
expect(isProtoSerializable(component)).toBe(false);
expect(getProtoName(component)).toBeUndefined();
});
});
describe('@ProtoField装饰器', () => {
it('应该正确定义字段', () => {
const definition = registry.getComponentDefinition('TestPosition');
expect(definition).toBeDefined();
expect(definition!.fields.size).toBe(3);
const xField = definition!.fields.get('x');
expect(xField).toEqual({
fieldNumber: 1,
type: ProtoFieldType.FLOAT,
repeated: false,
optional: false,
name: 'x'
});
const yField = definition!.fields.get('y');
expect(yField).toEqual({
fieldNumber: 2,
type: ProtoFieldType.FLOAT,
repeated: false,
optional: false,
name: 'y'
});
});
it('应该支持不同的字段类型', () => {
const definition = registry.getComponentDefinition('TestPlayer');
expect(definition).toBeDefined();
expect(definition!.fields.size).toBe(4);
const nameField = definition!.fields.get('name');
expect(nameField!.type).toBe(ProtoFieldType.STRING);
const levelField = definition!.fields.get('level');
expect(levelField!.type).toBe(ProtoFieldType.INT32);
const healthField = definition!.fields.get('health');
expect(healthField!.type).toBe(ProtoFieldType.INT32);
const isAliveField = definition!.fields.get('isAlive');
expect(isAliveField!.type).toBe(ProtoFieldType.BOOL);
});
it('应该检测字段编号冲突', () => {
const result = createConflictingComponent();
expect(result).toBeInstanceOf(Error);
expect((result as Error).message).toContain('字段编号 1 已被字段');
});
it('应该验证字段编号有效性', () => {
expect(() => {
class InvalidFieldComponent extends Component {
@ProtoField(0) // 无效的字段编号
public invalid: number = 0;
}
}).toThrow('字段编号必须大于0');
expect(() => {
class InvalidFieldComponent extends Component {
@ProtoField(-1) // 无效的字段编号
public invalid: number = 0;
}
}).toThrow('字段编号必须大于0');
});
});
describe('便捷装饰器', () => {
it('ProtoFloat应该设置正确的字段类型', () => {
@ProtoSerializable('FloatTest')
class FloatTestComponent extends Component {
@ProtoFloat(1)
public value: number = 0;
}
const definition = registry.getComponentDefinition('FloatTest');
const field = definition!.fields.get('value');
expect(field!.type).toBe(ProtoFieldType.FLOAT);
});
it('ProtoInt32应该设置正确的字段类型', () => {
@ProtoSerializable('Int32Test')
class Int32TestComponent extends Component {
@ProtoInt32(1)
public value: number = 0;
}
const definition = registry.getComponentDefinition('Int32Test');
const field = definition!.fields.get('value');
expect(field!.type).toBe(ProtoFieldType.INT32);
});
it('ProtoString应该设置正确的字段类型', () => {
@ProtoSerializable('StringTest')
class StringTestComponent extends Component {
@ProtoString(1)
public value: string = '';
}
const definition = registry.getComponentDefinition('StringTest');
const field = definition!.fields.get('value');
expect(field!.type).toBe(ProtoFieldType.STRING);
});
it('ProtoBool应该设置正确的字段类型', () => {
@ProtoSerializable('BoolTest')
class BoolTestComponent extends Component {
@ProtoBool(1)
public value: boolean = false;
}
const definition = registry.getComponentDefinition('BoolTest');
const field = definition!.fields.get('value');
expect(field!.type).toBe(ProtoFieldType.BOOL);
});
});
describe('ProtobufRegistry', () => {
it('应该正确生成proto定义', () => {
const protoDefinition = registry.generateProtoDefinition();
expect(protoDefinition).toContain('syntax = "proto3";');
expect(protoDefinition).toContain('package ecs;');
expect(protoDefinition).toContain('message TestPosition');
expect(protoDefinition).toContain('message TestPlayer');
expect(protoDefinition).toContain('float x = 1;');
expect(protoDefinition).toContain('float y = 2;');
expect(protoDefinition).toContain('string name = 1;');
expect(protoDefinition).toContain('int32 level = 2;');
expect(protoDefinition).toContain('bool isAlive = 4;');
});
it('应该正确管理组件注册', () => {
const allComponents = registry.getAllComponents();
expect(allComponents.size).toBeGreaterThanOrEqual(2);
expect(allComponents.has('TestPosition')).toBe(true);
expect(allComponents.has('TestPlayer')).toBe(true);
});
});
describe('字段选项', () => {
it('应该支持repeated字段', () => {
@ProtoSerializable('RepeatedTest')
class RepeatedTestComponent extends Component {
@ProtoField(1, ProtoFieldType.INT32, { repeated: true })
public values: number[] = [];
}
const definition = registry.getComponentDefinition('RepeatedTest');
const field = definition!.fields.get('values');
expect(field!.repeated).toBe(true);
});
it('应该支持optional字段', () => {
@ProtoSerializable('OptionalTest')
class OptionalTestComponent extends Component {
@ProtoField(1, ProtoFieldType.STRING, { optional: true })
public optionalValue?: string;
}
const definition = registry.getComponentDefinition('OptionalTest');
const field = definition!.fields.get('optionalValue');
expect(field!.optional).toBe(true);
});
});
});