Files
esengine/packages/core/tests/ECS/Hierarchy.test.ts
YHH b42a7b4e43 Feature/editor optimization (#251)
* refactor: 编辑器/运行时架构拆分与构建系统升级

* feat(core): 层级系统重构与UI变换矩阵修复

* refactor: 移除 ecs-components 聚合包并修复跨包组件查找问题

* fix(physics): 修复跨包组件类引用问题

* feat: 统一运行时架构与浏览器运行支持

* feat(asset): 实现浏览器运行时资产加载系统

* fix: 修复文档、CodeQL安全问题和CI类型检查错误

* fix: 修复文档、CodeQL安全问题和CI类型检查错误

* fix: 修复文档、CodeQL安全问题、CI类型检查和测试错误

* test: 补齐核心模块测试用例,修复CI构建配置

* fix: 修复测试用例中的类型错误和断言问题

* fix: 修复 turbo build:npm 任务的依赖顺序问题

* fix: 修复 CI 构建错误并优化构建性能
2025-12-01 22:28:51 +08:00

649 lines
24 KiB
TypeScript

import { Scene, Entity, HierarchyComponent, HierarchySystem } from '../../src';
describe('HierarchySystem', () => {
let scene: Scene;
let hierarchySystem: HierarchySystem;
beforeEach(() => {
scene = new Scene();
scene.initialize();
hierarchySystem = new HierarchySystem();
scene.addSystem(hierarchySystem);
});
afterEach(() => {
scene.end();
});
describe('setParent', () => {
it('should set parent-child relationship', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
expect(hierarchySystem.getParent(child)).toBe(parent);
expect(hierarchySystem.getChildren(parent)).toContain(child);
expect(hierarchySystem.getChildCount(parent)).toBe(1);
});
it('should auto-add HierarchyComponent if not present', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
expect(parent.getComponent(HierarchyComponent)).toBeNull();
expect(child.getComponent(HierarchyComponent)).toBeNull();
hierarchySystem.setParent(child, parent);
expect(parent.getComponent(HierarchyComponent)).not.toBeNull();
expect(child.getComponent(HierarchyComponent)).not.toBeNull();
});
it('should move child to root when parent is null', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
expect(hierarchySystem.getParent(child)).toBe(parent);
hierarchySystem.setParent(child, null);
expect(hierarchySystem.getParent(child)).toBeNull();
expect(hierarchySystem.getChildren(parent)).not.toContain(child);
});
it('should transfer child to new parent', () => {
const parent1 = scene.createEntity('Parent1');
const parent2 = scene.createEntity('Parent2');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent1);
expect(hierarchySystem.getParent(child)).toBe(parent1);
expect(hierarchySystem.getChildCount(parent1)).toBe(1);
hierarchySystem.setParent(child, parent2);
expect(hierarchySystem.getParent(child)).toBe(parent2);
expect(hierarchySystem.getChildCount(parent1)).toBe(0);
expect(hierarchySystem.getChildCount(parent2)).toBe(1);
});
it('should throw error on circular reference', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
const grandchild = scene.createEntity('Grandchild');
hierarchySystem.setParent(child, parent);
hierarchySystem.setParent(grandchild, child);
expect(() => {
hierarchySystem.setParent(parent, grandchild);
}).toThrow('Cannot set parent: would create circular reference');
});
it('should not change if setting same parent', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
const hierarchy = child.getComponent(HierarchyComponent)!;
hierarchy.bCacheDirty = false;
hierarchySystem.setParent(child, parent);
// Should not mark dirty since parent didn't change
expect(hierarchy.bCacheDirty).toBe(false);
});
});
describe('insertChildAt', () => {
it('should insert child at specific position', () => {
const parent = scene.createEntity('Parent');
const child1 = scene.createEntity('Child1');
const child2 = scene.createEntity('Child2');
const child3 = scene.createEntity('Child3');
hierarchySystem.setParent(child1, parent);
hierarchySystem.setParent(child3, parent);
hierarchySystem.insertChildAt(parent, child2, 1);
const children = hierarchySystem.getChildren(parent);
expect(children[0]).toBe(child1);
expect(children[1]).toBe(child2);
expect(children[2]).toBe(child3);
});
it('should append child when index is -1', () => {
const parent = scene.createEntity('Parent');
const child1 = scene.createEntity('Child1');
const child2 = scene.createEntity('Child2');
hierarchySystem.setParent(child1, parent);
hierarchySystem.insertChildAt(parent, child2, -1);
const children = hierarchySystem.getChildren(parent);
expect(children[children.length - 1]).toBe(child2);
});
});
describe('removeChild', () => {
it('should remove child from parent', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
expect(hierarchySystem.getChildCount(parent)).toBe(1);
const result = hierarchySystem.removeChild(parent, child);
expect(result).toBe(true);
expect(hierarchySystem.getChildCount(parent)).toBe(0);
expect(hierarchySystem.getParent(child)).toBeNull();
});
it('should return false if child is not a child of parent', () => {
const parent1 = scene.createEntity('Parent1');
const parent2 = scene.createEntity('Parent2');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent1);
const result = hierarchySystem.removeChild(parent2, child);
expect(result).toBe(false);
});
});
describe('removeAllChildren', () => {
it('should remove all children from parent', () => {
const parent = scene.createEntity('Parent');
const child1 = scene.createEntity('Child1');
const child2 = scene.createEntity('Child2');
const child3 = scene.createEntity('Child3');
hierarchySystem.setParent(child1, parent);
hierarchySystem.setParent(child2, parent);
hierarchySystem.setParent(child3, parent);
expect(hierarchySystem.getChildCount(parent)).toBe(3);
hierarchySystem.removeAllChildren(parent);
expect(hierarchySystem.getChildCount(parent)).toBe(0);
expect(hierarchySystem.getParent(child1)).toBeNull();
expect(hierarchySystem.getParent(child2)).toBeNull();
expect(hierarchySystem.getParent(child3)).toBeNull();
});
});
describe('hierarchy queries', () => {
it('should check if entity has children', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
expect(hierarchySystem.hasChildren(parent)).toBe(false);
hierarchySystem.setParent(child, parent);
expect(hierarchySystem.hasChildren(parent)).toBe(true);
});
it('should check isAncestorOf', () => {
const grandparent = scene.createEntity('Grandparent');
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(parent, grandparent);
hierarchySystem.setParent(child, parent);
expect(hierarchySystem.isAncestorOf(grandparent, child)).toBe(true);
expect(hierarchySystem.isAncestorOf(parent, child)).toBe(true);
expect(hierarchySystem.isAncestorOf(child, grandparent)).toBe(false);
});
it('should check isDescendantOf', () => {
const grandparent = scene.createEntity('Grandparent');
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(parent, grandparent);
hierarchySystem.setParent(child, parent);
expect(hierarchySystem.isDescendantOf(child, grandparent)).toBe(true);
expect(hierarchySystem.isDescendantOf(child, parent)).toBe(true);
expect(hierarchySystem.isDescendantOf(grandparent, child)).toBe(false);
});
it('should get root entity', () => {
const root = scene.createEntity('Root');
const child = scene.createEntity('Child');
const grandchild = scene.createEntity('Grandchild');
hierarchySystem.setParent(child, root);
hierarchySystem.setParent(grandchild, child);
expect(hierarchySystem.getRoot(grandchild)).toBe(root);
expect(hierarchySystem.getRoot(child)).toBe(root);
expect(hierarchySystem.getRoot(root)).toBe(root);
});
it('should get depth correctly', () => {
const root = scene.createEntity('Root');
const child = scene.createEntity('Child');
const grandchild = scene.createEntity('Grandchild');
root.addComponent(new HierarchyComponent());
hierarchySystem.setParent(child, root);
hierarchySystem.setParent(grandchild, child);
expect(hierarchySystem.getDepth(root)).toBe(0);
expect(hierarchySystem.getDepth(child)).toBe(1);
expect(hierarchySystem.getDepth(grandchild)).toBe(2);
});
});
describe('findChild', () => {
it('should find child by name', () => {
const parent = scene.createEntity('Parent');
const child1 = scene.createEntity('Child1');
const child2 = scene.createEntity('Target');
hierarchySystem.setParent(child1, parent);
hierarchySystem.setParent(child2, parent);
const found = hierarchySystem.findChild(parent, 'Target');
expect(found).toBe(child2);
});
it('should find child recursively', () => {
const root = scene.createEntity('Root');
const child = scene.createEntity('Child');
const grandchild = scene.createEntity('Target');
hierarchySystem.setParent(child, root);
hierarchySystem.setParent(grandchild, child);
const found = hierarchySystem.findChild(root, 'Target', true);
expect(found).toBe(grandchild);
const notFound = hierarchySystem.findChild(root, 'Target', false);
expect(notFound).toBeNull();
});
});
describe('forEachChild', () => {
it('should iterate over children', () => {
const parent = scene.createEntity('Parent');
const child1 = scene.createEntity('Child1');
const child2 = scene.createEntity('Child2');
hierarchySystem.setParent(child1, parent);
hierarchySystem.setParent(child2, parent);
const visited: Entity[] = [];
hierarchySystem.forEachChild(parent, (child) => {
visited.push(child);
});
expect(visited).toContain(child1);
expect(visited).toContain(child2);
expect(visited.length).toBe(2);
});
it('should iterate recursively', () => {
const root = scene.createEntity('Root');
const child = scene.createEntity('Child');
const grandchild = scene.createEntity('Grandchild');
hierarchySystem.setParent(child, root);
hierarchySystem.setParent(grandchild, child);
const visited: Entity[] = [];
hierarchySystem.forEachChild(root, (entity) => {
visited.push(entity);
}, true);
expect(visited).toContain(child);
expect(visited).toContain(grandchild);
expect(visited.length).toBe(2);
});
});
describe('getRootEntities', () => {
it('should return all root entities', () => {
const root1 = scene.createEntity('Root1');
const root2 = scene.createEntity('Root2');
const child = scene.createEntity('Child');
root1.addComponent(new HierarchyComponent());
root2.addComponent(new HierarchyComponent());
hierarchySystem.setParent(child, root1);
const roots = hierarchySystem.getRootEntities();
expect(roots).toContain(root1);
expect(roots).toContain(root2);
expect(roots).not.toContain(child);
});
});
describe('activeInHierarchy', () => {
it('should be inactive if parent is inactive', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
expect(hierarchySystem.isActiveInHierarchy(child)).toBe(true);
parent.active = false;
// Mark cache dirty to recalculate
const childHierarchy = child.getComponent(HierarchyComponent)!;
childHierarchy.bCacheDirty = true;
expect(hierarchySystem.isActiveInHierarchy(child)).toBe(false);
});
it('should be inactive if self is inactive', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
child.active = false;
expect(hierarchySystem.isActiveInHierarchy(child)).toBe(false);
});
});
});
describe('HierarchyComponent', () => {
it('should have correct default values', () => {
const component = new HierarchyComponent();
expect(component.parentId).toBeNull();
expect(component.childIds).toEqual([]);
expect(component.depth).toBe(0);
expect(component.bActiveInHierarchy).toBe(true);
expect(component.bCacheDirty).toBe(true);
});
});
describe('HierarchySystem - Extended Tests', () => {
let scene: Scene;
let hierarchySystem: HierarchySystem;
beforeEach(() => {
scene = new Scene();
scene.initialize();
hierarchySystem = new HierarchySystem();
scene.addSystem(hierarchySystem);
});
afterEach(() => {
scene.end();
});
describe('findChildrenByTag', () => {
it('should find children by tag', () => {
const parent = scene.createEntity('Parent');
const child1 = scene.createEntity('Child1');
const child2 = scene.createEntity('Child2');
const child3 = scene.createEntity('Child3');
child1.tag = 0x01;
child2.tag = 0x02;
child3.tag = 0x01;
hierarchySystem.setParent(child1, parent);
hierarchySystem.setParent(child2, parent);
hierarchySystem.setParent(child3, parent);
const found = hierarchySystem.findChildrenByTag(parent, 0x01);
expect(found.length).toBe(2);
expect(found).toContain(child1);
expect(found).toContain(child3);
});
it('should find children by tag recursively', () => {
const root = scene.createEntity('Root');
const child = scene.createEntity('Child');
const grandchild = scene.createEntity('Grandchild');
child.tag = 0x01;
grandchild.tag = 0x01;
hierarchySystem.setParent(child, root);
hierarchySystem.setParent(grandchild, child);
const foundNonRecursive = hierarchySystem.findChildrenByTag(root, 0x01, false);
expect(foundNonRecursive.length).toBe(1);
expect(foundNonRecursive[0]).toBe(child);
const foundRecursive = hierarchySystem.findChildrenByTag(root, 0x01, true);
expect(foundRecursive.length).toBe(2);
expect(foundRecursive).toContain(child);
expect(foundRecursive).toContain(grandchild);
});
it('should return empty array when no children match tag', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
child.tag = 0x01;
hierarchySystem.setParent(child, parent);
const found = hierarchySystem.findChildrenByTag(parent, 0x02);
expect(found).toEqual([]);
});
});
describe('flattenHierarchy', () => {
it('should flatten hierarchy with expanded nodes', () => {
const root = scene.createEntity('Root');
const child1 = scene.createEntity('Child1');
const child2 = scene.createEntity('Child2');
const grandchild = scene.createEntity('Grandchild');
root.addComponent(new HierarchyComponent());
hierarchySystem.setParent(child1, root);
hierarchySystem.setParent(child2, root);
hierarchySystem.setParent(grandchild, child1);
const expandedIds = new Set([root.id, child1.id]);
const flattened = hierarchySystem.flattenHierarchy(expandedIds);
expect(flattened.length).toBe(4);
expect(flattened[0].entity).toBe(root);
expect(flattened[0].depth).toBe(0);
expect(flattened[0].bHasChildren).toBe(true);
expect(flattened[0].bIsExpanded).toBe(true);
});
it('should not include children of collapsed nodes', () => {
const root = scene.createEntity('Root');
const child = scene.createEntity('Child');
const grandchild = scene.createEntity('Grandchild');
root.addComponent(new HierarchyComponent());
hierarchySystem.setParent(child, root);
hierarchySystem.setParent(grandchild, child);
// Root is expanded, but child is collapsed
const expandedIds = new Set([root.id]);
const flattened = hierarchySystem.flattenHierarchy(expandedIds);
expect(flattened.length).toBe(2);
expect(flattened[0].entity).toBe(root);
expect(flattened[1].entity).toBe(child);
expect(flattened[1].bHasChildren).toBe(true);
expect(flattened[1].bIsExpanded).toBe(false);
});
it('should return empty array when no root entities', () => {
const flattened = hierarchySystem.flattenHierarchy(new Set());
expect(flattened).toEqual([]);
});
});
describe('updateOrder', () => {
it('should have negative update order for early processing', () => {
expect(hierarchySystem.updateOrder).toBe(-1000);
});
});
describe('process - cache update', () => {
it('should update dirty caches during process', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
// Cache should be dirty after setParent
const childHierarchy = child.getComponent(HierarchyComponent)!;
expect(childHierarchy.bCacheDirty).toBe(true);
// Update scene to process
scene.update();
// Cache should be clean after process
expect(childHierarchy.bCacheDirty).toBe(false);
});
});
describe('insertChildAt edge cases', () => {
it('should handle circular reference prevention', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
const grandchild = scene.createEntity('Grandchild');
hierarchySystem.setParent(child, parent);
hierarchySystem.setParent(grandchild, child);
expect(() => {
hierarchySystem.insertChildAt(grandchild, parent, 0);
}).toThrow('Cannot set parent: would create circular reference');
});
it('should move child within same parent to different position', () => {
const parent = scene.createEntity('Parent');
const child1 = scene.createEntity('Child1');
const child2 = scene.createEntity('Child2');
const child3 = scene.createEntity('Child3');
hierarchySystem.setParent(child1, parent);
hierarchySystem.setParent(child2, parent);
hierarchySystem.setParent(child3, parent);
// Move child3 to position 0
hierarchySystem.insertChildAt(parent, child3, 0);
const children = hierarchySystem.getChildren(parent);
expect(children[0]).toBe(child3);
expect(children[1]).toBe(child1);
expect(children[2]).toBe(child2);
});
});
describe('removeChild edge cases', () => {
it('should return false when parent has no HierarchyComponent', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
const result = hierarchySystem.removeChild(parent, child);
expect(result).toBe(false);
});
it('should return false when child has no HierarchyComponent', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
parent.addComponent(new HierarchyComponent());
const result = hierarchySystem.removeChild(parent, child);
expect(result).toBe(false);
});
});
describe('removeAllChildren edge cases', () => {
it('should handle entity with no HierarchyComponent', () => {
const parent = scene.createEntity('Parent');
expect(() => {
hierarchySystem.removeAllChildren(parent);
}).not.toThrow();
});
});
describe('getChildren edge cases', () => {
it('should return empty array when entity has no HierarchyComponent', () => {
const entity = scene.createEntity('Entity');
const children = hierarchySystem.getChildren(entity);
expect(children).toEqual([]);
});
});
describe('getChildCount edge cases', () => {
it('should return 0 when entity has no HierarchyComponent', () => {
const entity = scene.createEntity('Entity');
expect(hierarchySystem.getChildCount(entity)).toBe(0);
});
});
describe('getDepth edge cases', () => {
it('should return 0 when entity has no HierarchyComponent', () => {
const entity = scene.createEntity('Entity');
expect(hierarchySystem.getDepth(entity)).toBe(0);
});
it('should use cached depth when cache is valid', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
parent.addComponent(new HierarchyComponent());
hierarchySystem.setParent(child, parent);
// First call computes depth
const depth1 = hierarchySystem.getDepth(child);
expect(depth1).toBe(1);
// Mark cache as valid
const childHierarchy = child.getComponent(HierarchyComponent)!;
childHierarchy.bCacheDirty = false;
// Second call should use cache
const depth2 = hierarchySystem.getDepth(child);
expect(depth2).toBe(1);
});
});
describe('isActiveInHierarchy edge cases', () => {
it('should return entity.active when entity has no HierarchyComponent', () => {
const entity = scene.createEntity('Entity');
entity.active = true;
expect(hierarchySystem.isActiveInHierarchy(entity)).toBe(true);
entity.active = false;
expect(hierarchySystem.isActiveInHierarchy(entity)).toBe(false);
});
it('should use cached value when cache is valid', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
// First call computes activeInHierarchy
const active1 = hierarchySystem.isActiveInHierarchy(child);
expect(active1).toBe(true);
// Mark cache as valid
const childHierarchy = child.getComponent(HierarchyComponent)!;
childHierarchy.bCacheDirty = false;
// Second call should use cache
const active2 = hierarchySystem.isActiveInHierarchy(child);
expect(active2).toBe(true);
});
});
describe('dispose', () => {
it('should not throw when disposing', () => {
expect(() => {
hierarchySystem.dispose();
}).not.toThrow();
});
});
});