import { describe, it, expect, beforeEach, vi } from "vitest"; import { System, SystemRegistry } from "../System"; import { ECSWorld } from "../World"; import { EventBus } from "../EventBus"; import { type EntityId } from "../../../core/types"; import { type ComponentType } from "../components"; // Test system implementations class TestSystemA extends System { readonly name = "TestA"; readonly requiredComponents: readonly ComponentType[] = ["position"]; readonly priority = 0; updateCalls: EntityId[][] = []; update(entities: EntityId[], _world: ECSWorld): void { this.updateCalls.push([...entities]); } } class TestSystemB extends System { readonly name = "TestB"; readonly requiredComponents: readonly ComponentType[] = ["position", "stats"]; readonly priority = 10; // Lower priority = runs later updateCalls: EntityId[][] = []; update(entities: EntityId[], _world: ECSWorld): void { this.updateCalls.push([...entities]); } } class TestSystemWithHooks extends System { readonly name = "TestWithHooks"; readonly requiredComponents: readonly ComponentType[] = ["position"]; registered = false; unregistered = false; addedEntities: EntityId[] = []; removedEntities: EntityId[] = []; update(_entities: EntityId[], _world: ECSWorld): void {} onRegister(_world: ECSWorld): void { this.registered = true; } onUnregister(_world: ECSWorld): void { this.unregistered = true; } onEntityAdded(entityId: EntityId, _world: ECSWorld): void { this.addedEntities.push(entityId); } onEntityRemoved(entityId: EntityId, _world: ECSWorld): void { this.removedEntities.push(entityId); } } describe("SystemRegistry", () => { let world: ECSWorld; let registry: SystemRegistry; beforeEach(() => { world = new ECSWorld(); registry = new SystemRegistry(world); }); describe("register()", () => { it("should register a system", () => { const system = new TestSystemA(); registry.register(system); expect(registry.has("TestA")).toBe(true); }); it("should call onRegister when registering", () => { const system = new TestSystemWithHooks(); registry.register(system); expect(system.registered).toBe(true); }); it("should inject event bus into system", () => { const eventBus = new EventBus(); const registryWithEvents = new SystemRegistry(world, eventBus); const system = new TestSystemA(); const setEventBusSpy = vi.spyOn(system, "setEventBus"); registryWithEvents.register(system); expect(setEventBusSpy).toHaveBeenCalledWith(eventBus); }); }); describe("unregister()", () => { it("should unregister by instance", () => { const system = new TestSystemA(); registry.register(system); const result = registry.unregister(system); expect(result).toBe(true); expect(registry.has("TestA")).toBe(false); }); it("should unregister by name", () => { registry.register(new TestSystemA()); const result = registry.unregister("TestA"); expect(result).toBe(true); expect(registry.has("TestA")).toBe(false); }); it("should call onUnregister when unregistering", () => { const system = new TestSystemWithHooks(); registry.register(system); registry.unregister(system); expect(system.unregistered).toBe(true); }); it("should return false for unknown system", () => { const result = registry.unregister("Unknown"); expect(result).toBe(false); }); }); describe("get()", () => { it("should return system by name", () => { const system = new TestSystemA(); registry.register(system); expect(registry.get("TestA")).toBe(system); }); it("should return undefined for unknown system", () => { expect(registry.get("Unknown")).toBeUndefined(); }); }); describe("updateAll()", () => { it("should update all systems", () => { const systemA = new TestSystemA(); const systemB = new TestSystemB(); registry.register(systemA); registry.register(systemB); // Create entity with position const id1 = world.createEntity(); world.addComponent(id1, "position", { x: 0, y: 0 }); registry.updateAll(); expect(systemA.updateCalls.length).toBe(1); expect(systemA.updateCalls[0]).toContain(id1); }); it("should pass only matching entities to each system", () => { const systemA = new TestSystemA(); // needs position const systemB = new TestSystemB(); // needs position + stats registry.register(systemA); registry.register(systemB); // Entity with only position const id1 = world.createEntity(); world.addComponent(id1, "position", { x: 0, y: 0 }); // Entity with position and stats const id2 = world.createEntity(); world.addComponent(id2, "position", { x: 1, y: 1 }); world.addComponent(id2, "stats", { hp: 10, maxHp: 10 } as any); registry.updateAll(); // SystemA should get both entities expect(systemA.updateCalls[0]).toContain(id1); expect(systemA.updateCalls[0]).toContain(id2); // SystemB should only get entity with both components expect(systemB.updateCalls[0]).not.toContain(id1); expect(systemB.updateCalls[0]).toContain(id2); }); it("should respect priority order", () => { const callOrder: string[] = []; class PrioritySystemLow extends System { readonly name = "Low"; readonly requiredComponents: readonly ComponentType[] = ["position"]; readonly priority = 100; update() { callOrder.push("Low"); } } class PrioritySystemHigh extends System { readonly name = "High"; readonly requiredComponents: readonly ComponentType[] = ["position"]; readonly priority = -10; update() { callOrder.push("High"); } } // Register in reverse order registry.register(new PrioritySystemLow()); registry.register(new PrioritySystemHigh()); const id = world.createEntity(); world.addComponent(id, "position", { x: 0, y: 0 }); registry.updateAll(); expect(callOrder).toEqual(["High", "Low"]); }); it("should skip disabled systems", () => { const system = new TestSystemA(); registry.register(system); const id = world.createEntity(); world.addComponent(id, "position", { x: 0, y: 0 }); system.enabled = false; registry.updateAll(); expect(system.updateCalls.length).toBe(0); }); }); describe("setEnabled()", () => { it("should enable/disable system by name", () => { const system = new TestSystemA(); registry.register(system); registry.setEnabled("TestA", false); expect(system.enabled).toBe(false); registry.setEnabled("TestA", true); expect(system.enabled).toBe(true); }); it("should return false for unknown system", () => { expect(registry.setEnabled("Unknown", false)).toBe(false); }); }); describe("entity notifications", () => { it("should notify systems when entity is added", () => { const system = new TestSystemWithHooks(); registry.register(system); const id = world.createEntity(); world.addComponent(id, "position", { x: 0, y: 0 }); registry.notifyEntityAdded(id); expect(system.addedEntities).toContain(id); }); it("should notify systems when entity is removed", () => { const system = new TestSystemWithHooks(); registry.register(system); const id = world.createEntity(); world.addComponent(id, "position", { x: 0, y: 0 }); registry.notifyEntityRemoved(id); expect(system.removedEntities).toContain(id); }); }); describe("getSystems()", () => { it("should return all registered systems", () => { registry.register(new TestSystemA()); registry.register(new TestSystemB()); const systems = registry.getSystems(); expect(systems.length).toBe(2); expect(systems.map(s => s.name)).toContain("TestA"); expect(systems.map(s => s.name)).toContain("TestB"); }); }); });