Add data validation tests for cards and enemies

This commit is contained in:
Jared Miller 2026-02-23 19:18:17 -05:00
parent a11439eb58
commit eb094a5138
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C

180
src/data.validation.test.js Normal file
View file

@ -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);
}
}
});
});