From eb094a51382d0ba49021f578abfc9ff31fb1e3d0 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Mon, 23 Feb 2026 19:18:17 -0500 Subject: [PATCH] Add data validation tests for cards and enemies --- src/data.validation.test.js | 180 ++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 src/data.validation.test.js diff --git a/src/data.validation.test.js b/src/data.validation.test.js new file mode 100644 index 0000000..faa4dfc --- /dev/null +++ b/src/data.validation.test.js @@ -0,0 +1,180 @@ +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); + } + } + }); +});