Add data validation tests for cards and enemies
This commit is contained in:
parent
a11439eb58
commit
eb094a5138
1 changed files with 180 additions and 0 deletions
180
src/data.validation.test.js
Normal file
180
src/data.validation.test.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue