import type { PlayerCharacter } from "@initiative/domain"; import { playerCharacterId } from "@initiative/domain"; import { beforeEach, describe, expect, it } from "vitest"; import { loadPlayerCharacters, savePlayerCharacters, } from "../player-character-storage.js"; const STORAGE_KEY = "initiative:player-characters"; function createMockLocalStorage() { const store = new Map(); return { getItem: (key: string) => store.get(key) ?? null, setItem: (key: string, value: string) => store.set(key, value), removeItem: (key: string) => store.delete(key), clear: () => store.clear(), get length() { return store.size; }, key: (_index: number) => null, store, }; } function makePC(overrides?: Partial): PlayerCharacter { return { id: playerCharacterId("pc-1"), name: "Aragorn", ac: 16, maxHp: 120, color: "green", icon: "sword", ...overrides, }; } describe("player-character-storage", () => { let mockStorage: ReturnType; beforeEach(() => { mockStorage = createMockLocalStorage(); Object.defineProperty(globalThis, "localStorage", { value: mockStorage, writable: true, }); }); describe("round-trip save/load", () => { it("saves and loads a single character", () => { const pc = makePC(); savePlayerCharacters([pc]); const loaded = loadPlayerCharacters(); expect(loaded).toEqual([pc]); }); it("saves and loads multiple characters", () => { const pcs = [ makePC({ id: playerCharacterId("pc-1"), name: "Aragorn" }), makePC({ id: playerCharacterId("pc-2"), name: "Legolas", ac: 14, maxHp: 90, color: "blue", icon: "eye", }), ]; savePlayerCharacters(pcs); const loaded = loadPlayerCharacters(); expect(loaded).toEqual(pcs); }); }); describe("empty storage", () => { it("returns empty array when no data exists", () => { expect(loadPlayerCharacters()).toEqual([]); }); }); describe("corrupt JSON", () => { it("returns empty array for invalid JSON", () => { mockStorage.setItem(STORAGE_KEY, "not-json{{{"); expect(loadPlayerCharacters()).toEqual([]); }); it("returns empty array for non-array JSON", () => { mockStorage.setItem(STORAGE_KEY, '{"foo": "bar"}'); expect(loadPlayerCharacters()).toEqual([]); }); }); describe("per-character validation", () => { it("discards character with missing name", () => { mockStorage.setItem( STORAGE_KEY, JSON.stringify([ { id: "pc-1", ac: 10, maxHp: 50, color: "blue", icon: "sword" }, ]), ); expect(loadPlayerCharacters()).toEqual([]); }); it("discards character with empty name", () => { mockStorage.setItem( STORAGE_KEY, JSON.stringify([ { id: "pc-1", name: "", ac: 10, maxHp: 50, color: "blue", icon: "sword", }, ]), ); expect(loadPlayerCharacters()).toEqual([]); }); it("discards character with invalid color", () => { mockStorage.setItem( STORAGE_KEY, JSON.stringify([ { id: "pc-1", name: "Test", ac: 10, maxHp: 50, color: "neon", icon: "sword", }, ]), ); expect(loadPlayerCharacters()).toEqual([]); }); it("discards character with invalid icon", () => { mockStorage.setItem( STORAGE_KEY, JSON.stringify([ { id: "pc-1", name: "Test", ac: 10, maxHp: 50, color: "blue", icon: "banana", }, ]), ); expect(loadPlayerCharacters()).toEqual([]); }); it("discards character with negative AC", () => { mockStorage.setItem( STORAGE_KEY, JSON.stringify([ { id: "pc-1", name: "Test", ac: -1, maxHp: 50, color: "blue", icon: "sword", }, ]), ); expect(loadPlayerCharacters()).toEqual([]); }); it("discards character with maxHp of 0", () => { mockStorage.setItem( STORAGE_KEY, JSON.stringify([ { id: "pc-1", name: "Test", ac: 10, maxHp: 0, color: "blue", icon: "sword", }, ]), ); expect(loadPlayerCharacters()).toEqual([]); }); it("keeps valid characters and discards invalid ones", () => { mockStorage.setItem( STORAGE_KEY, JSON.stringify([ { id: "pc-1", name: "Valid", ac: 10, maxHp: 50, color: "blue", icon: "sword", }, { id: "pc-2", name: "", ac: 10, maxHp: 50, color: "blue", icon: "sword", }, ]), ); const loaded = loadPlayerCharacters(); expect(loaded).toHaveLength(1); expect(loaded[0].name).toBe("Valid"); }); }); describe("storage errors", () => { it("save silently catches errors", () => { Object.defineProperty(globalThis, "localStorage", { value: { setItem: () => { throw new Error("QuotaExceeded"); }, getItem: () => null, }, writable: true, }); expect(() => savePlayerCharacters([makePC()])).not.toThrow(); }); }); });