363 lines
13 KiB
TypeScript
363 lines
13 KiB
TypeScript
|
|
/**
|
|||
|
|
* Matcher完整测试套件
|
|||
|
|
* 包含功能测试、性能测试和向后兼容性测试
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { Scene } from '../../../src/ECS/Scene';
|
|||
|
|
import { Entity } from '../../../src/ECS/Entity';
|
|||
|
|
import { Component } from '../../../src/ECS/Component';
|
|||
|
|
import { Matcher } from '../../../src/ECS/Utils/Matcher';
|
|||
|
|
|
|||
|
|
// 测试组件
|
|||
|
|
class Position extends Component {
|
|||
|
|
constructor(public x: number = 0, public y: number = 0) {
|
|||
|
|
super();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class Velocity extends Component {
|
|||
|
|
constructor(public vx: number = 0, public vy: number = 0) {
|
|||
|
|
super();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class Health extends Component {
|
|||
|
|
constructor(public hp: number = 100) {
|
|||
|
|
super();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class Dead extends Component {}
|
|||
|
|
|
|||
|
|
describe('Matcher测试套件', () => {
|
|||
|
|
let scene: Scene;
|
|||
|
|
let entities: Entity[];
|
|||
|
|
|
|||
|
|
beforeEach(() => {
|
|||
|
|
scene = new Scene();
|
|||
|
|
scene.begin();
|
|||
|
|
|
|||
|
|
// 创建测试实体
|
|||
|
|
entities = [];
|
|||
|
|
|
|||
|
|
// 实体1: 移动的活体
|
|||
|
|
const entity1 = scene.createEntity('MovingAlive');
|
|||
|
|
entity1.addComponent(new Position(10, 20));
|
|||
|
|
entity1.addComponent(new Velocity(1, 0));
|
|||
|
|
entity1.addComponent(new Health(100));
|
|||
|
|
entities.push(entity1);
|
|||
|
|
|
|||
|
|
// 实体2: 静止的活体
|
|||
|
|
const entity2 = scene.createEntity('StillAlive');
|
|||
|
|
entity2.addComponent(new Position(30, 40));
|
|||
|
|
entity2.addComponent(new Health(50));
|
|||
|
|
entities.push(entity2);
|
|||
|
|
|
|||
|
|
// 实体3: 移动的死体
|
|||
|
|
const entity3 = scene.createEntity('MovingDead');
|
|||
|
|
entity3.addComponent(new Position(50, 60));
|
|||
|
|
entity3.addComponent(new Velocity(0, 1));
|
|||
|
|
entity3.addComponent(new Dead());
|
|||
|
|
entities.push(entity3);
|
|||
|
|
|
|||
|
|
// 实体4: 静止的死体
|
|||
|
|
const entity4 = scene.createEntity('StillDead');
|
|||
|
|
entity4.addComponent(new Position(70, 80));
|
|||
|
|
entity4.addComponent(new Dead());
|
|||
|
|
entities.push(entity4);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
afterEach(() => {
|
|||
|
|
scene.end();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('新API测试', () => {
|
|||
|
|
test('create()应该创建有效的matcher', () => {
|
|||
|
|
const matcher = Matcher.create(scene.querySystem);
|
|||
|
|
expect(matcher).toBeInstanceOf(Matcher);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('all()查询应该正确工作', () => {
|
|||
|
|
const matcher = Matcher.create(scene.querySystem)
|
|||
|
|
.all(Position, Health);
|
|||
|
|
|
|||
|
|
const result = matcher.query();
|
|||
|
|
expect(result).toHaveLength(2);
|
|||
|
|
expect(result.map(e => e.name).sort()).toEqual(['MovingAlive', 'StillAlive']);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('any()查询应该正确工作', () => {
|
|||
|
|
const matcher = Matcher.create(scene.querySystem)
|
|||
|
|
.any(Health, Dead);
|
|||
|
|
|
|||
|
|
const result = matcher.query();
|
|||
|
|
expect(result).toHaveLength(4); // 所有实体
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('none()查询应该正确工作', () => {
|
|||
|
|
const matcher = Matcher.create(scene.querySystem)
|
|||
|
|
.all(Position)
|
|||
|
|
.none(Dead);
|
|||
|
|
|
|||
|
|
const result = matcher.query();
|
|||
|
|
expect(result).toHaveLength(2);
|
|||
|
|
expect(result.map(e => e.name).sort()).toEqual(['MovingAlive', 'StillAlive']);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('复合查询应该正确工作', () => {
|
|||
|
|
const matcher = Matcher.create(scene.querySystem)
|
|||
|
|
.all(Position)
|
|||
|
|
.any(Health, Velocity)
|
|||
|
|
.none(Dead);
|
|||
|
|
|
|||
|
|
const result = matcher.query();
|
|||
|
|
expect(result).toHaveLength(2);
|
|||
|
|
expect(result.map(e => e.name).sort()).toEqual(['MovingAlive', 'StillAlive']);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('matches()应该正确检查单个实体', () => {
|
|||
|
|
const matcher = Matcher.create(scene.querySystem)
|
|||
|
|
.all(Position, Velocity);
|
|||
|
|
|
|||
|
|
expect(matcher.matches(entities[0])).toBe(true); // MovingAlive
|
|||
|
|
expect(matcher.matches(entities[1])).toBe(false); // StillAlive
|
|||
|
|
expect(matcher.matches(entities[2])).toBe(true); // MovingDead
|
|||
|
|
expect(matcher.matches(entities[3])).toBe(false); // StillDead
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('count()和exists()应该正确工作', () => {
|
|||
|
|
const matcher = Matcher.create(scene.querySystem)
|
|||
|
|
.all(Health);
|
|||
|
|
|
|||
|
|
expect(matcher.count()).toBe(2);
|
|||
|
|
expect(matcher.exists()).toBe(true);
|
|||
|
|
|
|||
|
|
const emptyMatcher = Matcher.create(scene.querySystem)
|
|||
|
|
.all(Health, Dead);
|
|||
|
|
|
|||
|
|
expect(emptyMatcher.count()).toBe(0);
|
|||
|
|
expect(emptyMatcher.exists()).toBe(false);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('clone()应该创建独立的matcher', () => {
|
|||
|
|
const baseMatcher = Matcher.create(scene.querySystem)
|
|||
|
|
.all(Position);
|
|||
|
|
|
|||
|
|
const livingMatcher = baseMatcher.clone()
|
|||
|
|
.all(Health)
|
|||
|
|
.none(Dead);
|
|||
|
|
|
|||
|
|
const deadMatcher = baseMatcher.clone()
|
|||
|
|
.all(Dead);
|
|||
|
|
|
|||
|
|
expect(livingMatcher.count()).toBe(2);
|
|||
|
|
expect(deadMatcher.count()).toBe(2);
|
|||
|
|
expect(baseMatcher.count()).toBe(4); // 原始matcher不受影响
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('reset()应该清空所有条件', () => {
|
|||
|
|
const matcher = Matcher.create(scene.querySystem)
|
|||
|
|
.all(Position)
|
|||
|
|
.any(Health)
|
|||
|
|
.none(Dead);
|
|||
|
|
|
|||
|
|
expect(matcher.count()).toBe(2);
|
|||
|
|
|
|||
|
|
matcher.reset();
|
|||
|
|
expect(matcher.count()).toBe(4); // 所有实体
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('向后兼容性测试', () => {
|
|||
|
|
test('empty()和withQuerySystem()应该正常工作', () => {
|
|||
|
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|||
|
|
|
|||
|
|
const matcher = Matcher.empty()
|
|||
|
|
.all(Position, Health)
|
|||
|
|
.withQuerySystem(scene.querySystem);
|
|||
|
|
|
|||
|
|
const result = matcher.query();
|
|||
|
|
expect(result).toHaveLength(2);
|
|||
|
|
|
|||
|
|
// 应该有deprecation警告
|
|||
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|||
|
|
'withQuerySystem() is deprecated. Use Matcher.create(querySystem) instead.'
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
consoleSpy.mockRestore();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('deprecated方法应该工作并显示警告', () => {
|
|||
|
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|||
|
|
|
|||
|
|
const matcher = Matcher.empty()
|
|||
|
|
.all(Position)
|
|||
|
|
.withQuerySystem(scene.querySystem);
|
|||
|
|
|
|||
|
|
// 测试deprecated方法
|
|||
|
|
expect(matcher.isInterestedEntity(entities[0])).toBe(true);
|
|||
|
|
const result = matcher.queryEntities();
|
|||
|
|
expect(result).toHaveLength(4);
|
|||
|
|
|
|||
|
|
// 测试getter方法
|
|||
|
|
expect(matcher.getAllSet()).toEqual([Position]);
|
|||
|
|
expect(matcher.getExclusionSet()).toEqual([]);
|
|||
|
|
expect(matcher.getOneSet()).toEqual([]);
|
|||
|
|
|
|||
|
|
// 验证警告
|
|||
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|||
|
|
'isInterestedEntity() is deprecated. Use matches() instead.'
|
|||
|
|
);
|
|||
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|||
|
|
'queryEntities() is deprecated. Use query() instead.'
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
consoleSpy.mockRestore();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('无QuerySystem时应该抛出错误', () => {
|
|||
|
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|||
|
|
|
|||
|
|
const matcher = Matcher.empty()
|
|||
|
|
.all(Position, Health);
|
|||
|
|
|
|||
|
|
// 应该抛出错误而不是回退
|
|||
|
|
expect(() => matcher.matches(entities[0])).toThrow(
|
|||
|
|
'Matcher requires QuerySystem. Use Matcher.create(querySystem) or call withQuerySystem() first.'
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
expect(() => matcher.query()).toThrow(
|
|||
|
|
'Matcher requires QuerySystem. Use Matcher.create(querySystem) or call withQuerySystem() first.'
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
consoleSpy.mockRestore();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('新旧API应该产生相同结果', () => {
|
|||
|
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|||
|
|
|
|||
|
|
// 旧API
|
|||
|
|
const oldMatcher = Matcher.empty()
|
|||
|
|
.all(Position)
|
|||
|
|
.exclude(Dead)
|
|||
|
|
.withQuerySystem(scene.querySystem);
|
|||
|
|
|
|||
|
|
// 新API
|
|||
|
|
const newMatcher = Matcher.create(scene.querySystem)
|
|||
|
|
.all(Position)
|
|||
|
|
.none(Dead);
|
|||
|
|
|
|||
|
|
// 结果应该相同
|
|||
|
|
const oldResult = oldMatcher.query().sort((a, b) => a.id - b.id);
|
|||
|
|
const newResult = newMatcher.query().sort((a, b) => a.id - b.id);
|
|||
|
|
|
|||
|
|
expect(oldResult).toEqual(newResult);
|
|||
|
|
|
|||
|
|
// 单个实体检查也应该相同
|
|||
|
|
for (const entity of entities) {
|
|||
|
|
expect(oldMatcher.matches(entity)).toBe(newMatcher.matches(entity));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
consoleSpy.mockRestore();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('缓存机制测试', () => {
|
|||
|
|
test('条件变更应该使缓存失效', () => {
|
|||
|
|
const matcher = Matcher.create(scene.querySystem)
|
|||
|
|
.all(Position);
|
|||
|
|
|
|||
|
|
const result1 = matcher.query();
|
|||
|
|
|
|||
|
|
// 添加条件
|
|||
|
|
matcher.all(Health);
|
|||
|
|
const result2 = matcher.query();
|
|||
|
|
|
|||
|
|
// 结果应该不同
|
|||
|
|
expect(result2.length).toBeLessThan(result1.length);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('QuerySystem版本变更应该使缓存失效', () => {
|
|||
|
|
const matcher = Matcher.create(scene.querySystem)
|
|||
|
|
.all(Position);
|
|||
|
|
|
|||
|
|
const result1 = matcher.query();
|
|||
|
|
|
|||
|
|
// 添加新实体触发版本变更
|
|||
|
|
const newEntity = scene.createEntity('NewEntity');
|
|||
|
|
newEntity.addComponent(new Position(100, 100));
|
|||
|
|
|
|||
|
|
const result2 = matcher.query();
|
|||
|
|
|
|||
|
|
// 结果应该包含新实体
|
|||
|
|
expect(result2.length).toBe(result1.length + 1);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('重复查询应该使用缓存', () => {
|
|||
|
|
const matcher = Matcher.create(scene.querySystem)
|
|||
|
|
.all(Position);
|
|||
|
|
|
|||
|
|
const result1 = matcher.query();
|
|||
|
|
const result2 = matcher.query();
|
|||
|
|
|
|||
|
|
// 结果应该相同(功能测试,不测性能)
|
|||
|
|
expect(result1).toEqual(result2);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('边界情况测试', () => {
|
|||
|
|
test('空条件应该返回所有实体', () => {
|
|||
|
|
const matcher = Matcher.create(scene.querySystem);
|
|||
|
|
const result = matcher.query();
|
|||
|
|
expect(result.length).toBeGreaterThan(0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('不存在的组件查询应该返回空结果', () => {
|
|||
|
|
class NonExistentComponent extends Component {}
|
|||
|
|
|
|||
|
|
const matcher = Matcher.create(scene.querySystem)
|
|||
|
|
.all(NonExistentComponent);
|
|||
|
|
|
|||
|
|
expect(matcher.query()).toEqual([]);
|
|||
|
|
expect(matcher.count()).toBe(0);
|
|||
|
|
expect(matcher.exists()).toBe(false);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('复杂的排除条件应该正确工作', () => {
|
|||
|
|
const matcher = Matcher.create(scene.querySystem)
|
|||
|
|
.all(Position)
|
|||
|
|
.none(Health, Dead); // 排除有血量或死亡的
|
|||
|
|
|
|||
|
|
// 应该没有结果,因为所有有Position的实体都有Health或Dead
|
|||
|
|
expect(matcher.query()).toEqual([]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('toString()应该提供有用的描述', () => {
|
|||
|
|
const matcher = Matcher.create(scene.querySystem)
|
|||
|
|
.all(Position, Health)
|
|||
|
|
.any(Velocity)
|
|||
|
|
.none(Dead);
|
|||
|
|
|
|||
|
|
const description = matcher.toString();
|
|||
|
|
expect(description).toContain('all(Position, Health)');
|
|||
|
|
expect(description).toContain('any(Velocity)');
|
|||
|
|
expect(description).toContain('none(Dead)');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('getCondition()应该返回只读条件', () => {
|
|||
|
|
const matcher = Matcher.create(scene.querySystem)
|
|||
|
|
.all(Position)
|
|||
|
|
.any(Health)
|
|||
|
|
.none(Dead);
|
|||
|
|
|
|||
|
|
const condition = matcher.getCondition();
|
|||
|
|
expect(condition.all).toEqual([Position]);
|
|||
|
|
expect(condition.any).toEqual([Health]);
|
|||
|
|
expect(condition.none).toEqual([Dead]);
|
|||
|
|
|
|||
|
|
// 修改返回的条件不应该影响原matcher
|
|||
|
|
condition.all.push(Velocity as any);
|
|||
|
|
expect(matcher.getCondition().all).toEqual([Position]);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|