import { describe, expect, test } from "bun:test"; import { existsSync } from "node:fs"; import { resolve } from "node:path"; import allCards from "../data/cards.json"; import allEnemies from "../data/enemies.json"; const projectRoot = resolve(import.meta.dir, ".."); const VALID_CHARACTERS = [ "ironclad", "silent", "defect", "watcher", "colorless", ]; const VALID_RARITIES = ["starter", "common", "uncommon", "rare", "special"]; const VALID_ACTS = [1, 2, 3, 4]; describe("cards.json validation", () => { const cards = Object.entries(allCards); test("every card has required fields", () => { const required = [ "id", "name", "character", "rarity", "type", "cost", "effects", ]; for (const [, card] of cards) { for (const field of required) { expect(card).toHaveProperty(field); } } }); test("every card id matches its object key", () => { for (const [key, card] of cards) { expect(card.id).toBe(key); } }); test("every card character is valid", () => { for (const [, card] of cards) { expect(VALID_CHARACTERS).toContain(card.character); } }); test("every card rarity is valid", () => { for (const [, card] of cards) { expect(VALID_RARITIES).toContain(card.rarity); } }); test("every upgrade reference points to an existing card id", () => { for (const [, card] of cards) { if (card.upgraded) { expect(allCards).toHaveProperty(card.upgraded); } } }); test("every image path resolves to a real file", () => { for (const [, card] of cards) { if (card.image) { const abs = resolve(projectRoot, card.image); expect(existsSync(abs)).toBe(true); } } }); // copies is optional and only applies to starter deck cards (8 total) test("copies when present is a positive integer", () => { for (const [, card] of cards) { if (card.copies !== undefined) { expect(typeof card.copies).toBe("number"); expect(card.copies).toBeGreaterThan(0); } } }); test("image coverage report", () => { const cardList = Object.values(allCards); const withImages = cardList.filter((c) => c.image); const without = cardList.filter((c) => !c.image); console.debug(`Image coverage: ${withImages.length}/${cardList.length}`); const byChar = {}; for (const c of without) { byChar[c.character] = (byChar[c.character] || 0) + 1; } if (Object.keys(byChar).length > 0) { console.debug("Missing images by character:", byChar); } // informational only — no assertion }); }); describe("enemies.json validation", () => { const enemies = Object.entries(allEnemies); test("every enemy has required fields", () => { const required = ["id", "name", "hp", "act", "actionType"]; for (const [, enemy] of enemies) { for (const field of required) { expect(enemy).toHaveProperty(field); } } }); test("every enemy id matches its object key", () => { for (const [key, enemy] of enemies) { expect(enemy.id).toBe(key); } }); test("every enemy act is valid", () => { for (const [, enemy] of enemies) { expect(VALID_ACTS).toContain(enemy.act); } }); test("die-type enemies have exactly 6 actions keyed 1 through 6", () => { for (const [, enemy] of enemies) { if (enemy.actionType === "die") { expect(enemy.actions).toBeDefined(); const keys = Object.keys(enemy.actions); expect(keys.sort()).toEqual(["1", "2", "3", "4", "5", "6"]); } } }); test("cube-type enemies have an actionTrack array", () => { for (const [, enemy] of enemies) { if (enemy.actionType === "cube") { expect(Array.isArray(enemy.actionTrack)).toBe(true); expect(enemy.actionTrack.length).toBeGreaterThan(0); } } }); test("every action has an intent and effects array", () => { for (const [, enemy] of enemies) { const actions = enemy.actionType === "cube" ? enemy.actionTrack : Object.values(enemy.actions ?? {}); for (const action of actions) { expect(action).toHaveProperty("intent"); expect(Array.isArray(action.effects)).toBe(true); } } }); test("each effect has type and value fields", () => { for (const [, enemy] of enemies) { const actions = enemy.actionType === "cube" ? enemy.actionTrack : Object.values(enemy.actions ?? {}); for (const action of actions) { for (const effect of action.effects) { expect(effect).toHaveProperty("type"); expect(effect).toHaveProperty("value"); } } } }); test("notes when present is a non-empty string", () => { for (const [, enemy] of enemies) { if (enemy.notes !== undefined) { expect(typeof enemy.notes).toBe("string"); expect(enemy.notes.length).toBeGreaterThan(0); } } }); });