import { describe, it, expect, beforeEach } from "vitest"; import { World, defineComponent, defineRelationship, type WorldSnapshot, query, } from "../src/index"; // ── Definitions ───────────────────────────────────── const Position = defineComponent("position", { x: 0, y: 0 }); const Velocity = defineComponent("velocity", { vx: 0, vy: 0 }); const Health = defineComponent("health", { current: 100, max: 100 }); const ChildOf = defineRelationship("childOf"); const ParentOf = defineRelationship("parentOf"); describe("Serialization", () => { let world: World; beforeEach(() => { world = new World(); }); it("serializes components by name", () => { const e = world.spawn(); world.add(e, Position, { x: 10, y: 20 }); world.add(e, Velocity, { vx: 1, vy: 0 }); const snap = world.toJSON(); expect(snap.entities).toHaveProperty("e0"); expect(snap.entities.e0.position).toEqual({ x: 10, y: 20 }); expect(snap.entities.e0.velocity).toEqual({ vx: 1, vy: 0 }); }); it("round-trips components through JSON", () => { const e = world.spawn(); world.add(e, Position, { x: 42, y: 99 }); world.add(e, Velocity, { vx: 2, vy: -1 }); const json = JSON.stringify(world.toJSON()); const data = JSON.parse(json) as WorldSnapshot; const loaded = World.fromJSON(data, [Position, Velocity]); // Find entity via query const loadedEnts = [...loaded.query(query(Position, Velocity))]; expect(loadedEnts).toHaveLength(1); const loadedE = loadedEnts[0]; expect(loaded.get(loadedE, Position)).toEqual({ x: 42, y: 99 }); expect(loaded.get(loadedE, Velocity)).toEqual({ vx: 2, vy: -1 }); }); it("handles multiple entities", () => { world.spawn(); world.spawn(); const snap = world.toJSON(); // Bare entities still appear const entries = Object.entries(snap.entities); expect(entries).toHaveLength(2); }); it("serializes relationships", () => { const parent = world.spawn(); const child = world.spawn(); world.relate(child, ChildOf, parent); const snap = world.toJSON(); expect(snap.relationships.childOf).toHaveProperty("e1"); expect(snap.relationships.childOf.e1).toBe("e0"); }); it("round-trips relationships", () => { const parent = world.spawn(); const child = world.spawn(); world.relate(child, ChildOf, parent); const json = JSON.stringify(world.toJSON()); const data = JSON.parse(json) as WorldSnapshot; const loaded = World.fromJSON(data, [Position], [ChildOf, ParentOf]); // Verify via the re-serialized snapshot const reSnap = loaded.toJSON(); expect(Object.keys(reSnap.relationships.childOf)).toHaveLength(1); }); it("throws on unknown component in snapshot", () => { const snap: WorldSnapshot = { entities: { e0: { unknownComp: { x: 1 } } }, relationships: {}, }; expect(() => World.fromJSON(snap, [Position])).toThrow( 'Unknown component "unknownComp"', ); }); it("throws on unknown relationship in snapshot", () => { const snap: WorldSnapshot = { entities: { e0: { position: { x: 0, y: 0 } } }, relationships: { unknownRel: { e0: "e1" } }, }; expect(() => World.fromJSON(snap, [Position], [ChildOf])).toThrow( 'Unknown relationship "unknownRel"', ); }); it("preserves entities with no components", () => { world.spawn(); const snap = world.toJSON(); expect(snap.entities).toHaveProperty("e0"); expect(snap.entities.e0).toEqual({}); }); it("preserves bare entities on round-trip", () => { world.spawn(); const json = JSON.stringify(world.toJSON()); const loaded = World.fromJSON(JSON.parse(json), [Position]); expect(loaded.entityCount).toBe(1); // Entity has no Position, so query returns empty const withPos = [...loaded.query(query(Position))]; expect(withPos).toHaveLength(0); // But it exists expect(loaded.entityCount).toBe(1); }); it("entity IDs are re-sequential (no hole preservation)", () => { const a = world.spawn(); const b = world.spawn(); const c = world.spawn(); world.destroy(b); // creates a hole in the original world const snap = world.toJSON(); const ids = Object.keys(snap.entities).sort(); // Serialized as e0 and e1 (hole is collapsed) expect(ids).toHaveLength(2); }); it("round-trips JSON stringify and parse", () => { const a = world.spawn(); world.add(a, Position, { x: 10, y: 20 }); world.add(a, Health, { current: 75, max: 100 }); const raw = JSON.stringify(world.toJSON()); const parsed = JSON.parse(raw); const fresh = World.fromJSON(parsed, [Position, Health]); const reSnap = fresh.toJSON(); expect(reSnap.entities.e0.position).toEqual({ x: 10, y: 20 }); expect(reSnap.entities.e0.health).toEqual({ current: 75, max: 100 }); }); it("empty world serializes to empty snapshot", () => { const snap = world.toJSON(); expect(snap.entities).toEqual({}); expect(snap.relationships).toEqual({}); }); });