import { describe, expect, it } from "vitest"; import { rehydrateCombatant } from "../rehydrate-combatant.js"; function validCombatant(overrides: Record = {}) { return { id: "c-1", name: "Goblin", initiative: 12, ac: 15, maxHp: 7, currentHp: 5, tempHp: 3, conditions: ["poisoned"], isConcentrating: true, creatureId: "creature-goblin", color: "red", icon: "skull", playerCharacterId: "pc-1", ...overrides, }; } function minimalCombatant() { return { id: "c-1", name: "Goblin" }; } describe("rehydrateCombatant", () => { describe("valid input", () => { it("accepts a combatant with all fields", () => { const result = rehydrateCombatant(validCombatant()); expect(result).not.toBeNull(); expect(result?.name).toBe("Goblin"); expect(result?.initiative).toBe(12); expect(result?.ac).toBe(15); expect(result?.maxHp).toBe(7); expect(result?.currentHp).toBe(5); expect(result?.tempHp).toBe(3); expect(result?.conditions).toEqual(["poisoned"]); expect(result?.isConcentrating).toBe(true); expect(result?.creatureId).toBe("creature-goblin"); expect(result?.color).toBe("red"); expect(result?.icon).toBe("skull"); expect(result?.playerCharacterId).toBe("pc-1"); }); it("accepts a minimal combatant (id + name only)", () => { const result = rehydrateCombatant(minimalCombatant()); expect(result).not.toBeNull(); expect(result?.id).toBe("c-1"); expect(result?.name).toBe("Goblin"); expect(result?.initiative).toBeUndefined(); expect(result?.ac).toBeUndefined(); expect(result?.maxHp).toBeUndefined(); }); it("preserves branded CombatantId", () => { const result = rehydrateCombatant(minimalCombatant()); expect(result?.id).toBe("c-1"); }); }); describe("required field rejection", () => { it.each([ null, 42, "string", [1, 2], undefined, ])("rejects non-object input: %j", (input) => { expect(rehydrateCombatant(input)).toBeNull(); }); it("rejects missing id", () => { const { id: _, ...rest } = minimalCombatant(); expect(rehydrateCombatant(rest)).toBeNull(); }); it("rejects empty id", () => { expect(rehydrateCombatant({ ...minimalCombatant(), id: "" })).toBeNull(); }); it("rejects missing name", () => { const { name: _, ...rest } = minimalCombatant(); expect(rehydrateCombatant(rest)).toBeNull(); }); it("rejects non-string name", () => { expect( rehydrateCombatant({ ...minimalCombatant(), name: 42 }), ).toBeNull(); expect( rehydrateCombatant({ ...minimalCombatant(), name: null }), ).toBeNull(); }); }); describe("optional field leniency", () => { it("drops invalid ac — keeps combatant", () => { for (const ac of [-1, 1.5, "15"]) { const result = rehydrateCombatant({ ...minimalCombatant(), ac }); expect(result).not.toBeNull(); expect(result?.ac).toBeUndefined(); } }); it("drops invalid maxHp — keeps combatant", () => { for (const maxHp of [0, 1.5, "7"]) { const result = rehydrateCombatant({ ...minimalCombatant(), maxHp }); expect(result).not.toBeNull(); expect(result?.maxHp).toBeUndefined(); } }); it("falls back currentHp to maxHp when currentHp invalid", () => { const result = rehydrateCombatant({ ...minimalCombatant(), maxHp: 10, currentHp: "bad", }); expect(result?.maxHp).toBe(10); expect(result?.currentHp).toBe(10); }); it("falls back currentHp to maxHp when currentHp > maxHp", () => { const result = rehydrateCombatant({ ...minimalCombatant(), maxHp: 10, currentHp: 15, }); expect(result?.maxHp).toBe(10); expect(result?.currentHp).toBe(10); }); it("drops invalid initiative — keeps combatant", () => { for (const initiative of [1.5, "12"]) { const result = rehydrateCombatant({ ...minimalCombatant(), initiative, }); expect(result).not.toBeNull(); expect(result?.initiative).toBeUndefined(); } }); it("drops invalid conditions — keeps combatant", () => { const result = rehydrateCombatant({ ...minimalCombatant(), conditions: "poisoned", }); expect(result).not.toBeNull(); expect(result?.conditions).toBeUndefined(); }); it("drops unknown condition IDs", () => { const result = rehydrateCombatant({ ...minimalCombatant(), conditions: ["fake-condition"], }); expect(result).not.toBeNull(); expect(result?.conditions).toBeUndefined(); }); it("filters valid conditions from mixed array", () => { const result = rehydrateCombatant({ ...minimalCombatant(), conditions: ["poisoned", "fake", "blinded"], }); expect(result?.conditions).toEqual(["poisoned", "blinded"]); }); it("drops invalid color — keeps combatant", () => { const result = rehydrateCombatant({ ...minimalCombatant(), color: "neon", }); expect(result).not.toBeNull(); expect(result?.color).toBeUndefined(); }); it("drops invalid icon — keeps combatant", () => { const result = rehydrateCombatant({ ...minimalCombatant(), icon: "rocket", }); expect(result).not.toBeNull(); expect(result?.icon).toBeUndefined(); }); it("drops isConcentrating when not strictly true", () => { for (const isConcentrating of [false, "true", 1]) { const result = rehydrateCombatant({ ...minimalCombatant(), isConcentrating, }); expect(result).not.toBeNull(); expect(result?.isConcentrating).toBeUndefined(); } }); it("drops invalid creatureId", () => { for (const creatureId of ["", 42]) { const result = rehydrateCombatant({ ...minimalCombatant(), creatureId, }); expect(result).not.toBeNull(); expect(result?.creatureId).toBeUndefined(); } }); it("drops invalid playerCharacterId", () => { for (const playerCharacterId of ["", 42]) { const result = rehydrateCombatant({ ...minimalCombatant(), playerCharacterId, }); expect(result).not.toBeNull(); expect(result?.playerCharacterId).toBeUndefined(); } }); it("preserves valid cr field", () => { for (const cr of ["5", "1/4", "0", "30"]) { const result = rehydrateCombatant({ ...minimalCombatant(), cr }); expect(result).not.toBeNull(); expect(result?.cr).toBe(cr); } }); it("drops invalid cr field", () => { for (const cr of ["99", "", 42, null, "abc"]) { const result = rehydrateCombatant({ ...minimalCombatant(), cr }); expect(result).not.toBeNull(); expect(result?.cr).toBeUndefined(); } }); it("combatant without cr rehydrates as before", () => { const result = rehydrateCombatant(minimalCombatant()); expect(result).not.toBeNull(); expect(result?.cr).toBeUndefined(); }); it("preserves valid side field", () => { for (const side of ["party", "enemy"]) { const result = rehydrateCombatant({ ...minimalCombatant(), side }); expect(result).not.toBeNull(); expect(result?.side).toBe(side); } }); it("drops invalid side field", () => { for (const side of ["ally", "", 42, null, true]) { const result = rehydrateCombatant({ ...minimalCombatant(), side }); expect(result).not.toBeNull(); expect(result?.side).toBeUndefined(); } }); it("combatant without side rehydrates as before", () => { const result = rehydrateCombatant(minimalCombatant()); expect(result).not.toBeNull(); expect(result?.side).toBeUndefined(); }); it("drops invalid tempHp — keeps combatant", () => { for (const tempHp of [-1, 1.5, "3"]) { const result = rehydrateCombatant({ ...minimalCombatant(), tempHp, }); expect(result).not.toBeNull(); expect(result?.tempHp).toBeUndefined(); } }); it("preserves valid tempHp of 0", () => { const result = rehydrateCombatant({ ...minimalCombatant(), tempHp: 0, }); expect(result?.tempHp).toBe(0); }); }); });