127 lines
7.4 KiB
JavaScript
127 lines
7.4 KiB
JavaScript
// Rôle : vérifie les données Aniimo et le filtrage des variantes avant regroupement.
|
|
import { readFile, access } from "node:fs/promises";
|
|
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { createAniimoFilters, filterAniimoVariants, groupAniimoVariants, prepareAniimoData } from "../website/src/features/games/aniimo/utils.js";
|
|
import { loadAniimoData } from "../website/src/features/games/loaders.js";
|
|
import { validateAniimoData } from "./helpers/data-validation.mjs";
|
|
|
|
const readJson = async (path) => JSON.parse(await readFile(`website/public/data/${path}`, "utf8"));
|
|
const [aniimos, fr, en] = await Promise.all([readJson("aniimo/aniimos.json"), readJson("aniimo/i18n/fr.json"), readJson("aniimo/i18n/en.json")]);
|
|
const data = prepareAniimoData(aniimos, fr, en);
|
|
|
|
test("Aniimo attributes have valid stats and matching totals", () => {
|
|
validateAniimoData(aniimos, [fr, en]);
|
|
const invalid = structuredClone(aniimos[0]);
|
|
invalid.variations[0].attributes.total++;
|
|
assert.throws(() => validateAniimoData([invalid], [fr, en]), /Stats total/);
|
|
invalid.variations[0].attributes.stats.hp = -1;
|
|
assert.throws(() => validateAniimoData([invalid], [fr, en]), /nonnegative integers/);
|
|
});
|
|
const filter = (overrides, variants = data.variants) => filterAniimoVariants(variants, { ...createAniimoFilters(), ...overrides });
|
|
|
|
test("Aniimo data has valid variants, translations, portraits and icons", async () => {
|
|
validateAniimoData(aniimos, [fr, en]);
|
|
const game = (await readJson("games.json")).games.find((game) => game.id === "aniimo");
|
|
assert.ok(game);
|
|
assert.equal(game.images.aniimosCover, "/static/img/aniimo/emberpup__basic_form.png");
|
|
const assets = new Set([game.images.cardCover, game.images.heroBg, game.images.aniimosCover, ...data.variants.map((variant) => variant.image)]);
|
|
for (const option of Object.values(data.filterOptions).flat()) assets.add(`/static/img/aniimo/${option}.png`);
|
|
await Promise.all([...assets].map((asset) => access(`website/public${asset}`)));
|
|
for (const variant of data.variants) {
|
|
const png = await readFile(`website/public${variant.image}`);
|
|
assert.equal(png.readUInt32BE(16), 512);
|
|
assert.equal(png.readUInt32BE(20), 840);
|
|
}
|
|
});
|
|
|
|
test("validation rejects duplicate forms, missing translations and invalid levels", () => {
|
|
const entry = structuredClone(aniimos[0]);
|
|
entry.variations.push(entry.variations[0]);
|
|
assert.throws(() => validateAniimoData([entry], [fr, en]), /Duplicate form/);
|
|
assert.throws(() => validateAniimoData(aniimos, [{}]), /must be a string/);
|
|
for (const level of [0, 5, 1.5, "3"]) {
|
|
const invalid = structuredClone(aniimos[0]);
|
|
invalid.variations[0].homeland_abilities[0].level = level;
|
|
assert.throws(() => validateAniimoData([invalid], [fr, en]), /integer between 1 and 4/);
|
|
}
|
|
});
|
|
|
|
test("name searches accept French, English, accents and case", () => {
|
|
assert.deepEqual(filter({ name: " LOUFEUTEAU " }).map((v) => v.key), filter({ name: "Emberpup" }).map((v) => v.key));
|
|
assert.ok(filter({ name: "celestia" }).some((variant) => variant.name === "celestis"));
|
|
assert.ok(filter({ name: "pomoeuf" }).some((variant) => variant.name === "pomegg"));
|
|
assert.deepEqual(filter({ name: "inexistant-xyz" }), []);
|
|
});
|
|
|
|
test("Prismana-only filtering combines with other criteria before grouping", () => {
|
|
assert.equal(createAniimoFilters().prismanaOnly, false);
|
|
const expected = data.variants.filter((variant) => variant.form === "prismana_form");
|
|
assert.ok(expected.length > 0);
|
|
assert.deepEqual(filter({ prismanaOnly: true }), expected);
|
|
const variant = expected[0];
|
|
const criteria = { name: variant.name, elements: variant.elements, roles: [variant.role], abilities: Object.fromEntries(variant.homeland_abilities.map(({ type, level }) => [type, level])) };
|
|
assert.deepEqual(filter({ ...criteria, prismanaOnly: true }), [variant]);
|
|
assert.equal(groupAniimoVariants(expected).length, expected.length);
|
|
assert.deepEqual(filter({ prismanaOnly: true, name: "emberpup" }), []);
|
|
assert.equal(filter({ prismanaOnly: false }).length, data.variants.length);
|
|
});
|
|
|
|
test("Prismana remains the final variation in data and grouped results", () => {
|
|
for (const group of groupAniimoVariants(data.variants)) {
|
|
if (group.some((variant) => variant.form === "prismana_form")) assert.equal(group.at(-1).form, "prismana_form");
|
|
}
|
|
const entry = structuredClone(aniimos.find((aniimo) => aniimo.variations.length > 1 && aniimo.variations.at(-1).form === "prismana_form"));
|
|
entry.variations.unshift(entry.variations.pop());
|
|
assert.throws(() => validateAniimoData([entry], [fr, en]), /Prismana must be last/);
|
|
});
|
|
|
|
const fixture = prepareAniimoData([{
|
|
id: "001", name: "example", elements: ["rock", "water"], role: "support",
|
|
variations: [
|
|
{ form: "rock_form", elements: ["rock"], role: "dps", homeland_abilities: [{ type: "carry", level: 3 }] },
|
|
{ form: "water_form", elements: ["water"], role: "heal", homeland_abilities: [{ type: "crafting", level: 2 }] },
|
|
{ form: "both_form", elements: ["rock", "water"], role: "dps", homeland_abilities: [{ type: "carry", level: 4 }, { type: "crafting", level: 1 }] }
|
|
]
|
|
}], {}, {}).variants;
|
|
|
|
test("element AND never borrows elements or roles from the parent or sibling variants", () => {
|
|
assert.deepEqual(filter({ elements: ["rock", "water"] }, fixture).map((v) => v.form), ["both_form"]);
|
|
assert.equal(filter({ elements: ["rock", "water"], elementsLogic: "or" }, fixture).length, 3);
|
|
assert.equal(filter({ roles: ["support"] }, fixture).length, 0);
|
|
assert.equal(filter({ roles: ["dps", "heal"] }, fixture).length, 3);
|
|
});
|
|
|
|
test("ability thresholds remain independent, inclusive and constrained to a single variant", () => {
|
|
assert.equal(filter({ abilities: { carry: 3, crafting: 2 } }, fixture).length, 0);
|
|
assert.equal(filter({ abilities: { carry: 3, crafting: 2 }, abilitiesLogic: "or" }, fixture).length, 3);
|
|
assert.deepEqual(filter({ abilities: { carry: 4, crafting: 2 }, abilitiesLogic: "or" }, fixture).map((v) => v.form), ["water_form", "both_form"]);
|
|
assert.deepEqual(filter({ abilities: { carry: 3, crafting: 1 } }, fixture).map((v) => v.form), ["both_form"]);
|
|
assert.equal(filter({ elements: ["rock"], roles: ["heal"], abilitiesLogic: "or", abilities: { carry: 1, crafting: 1 } }, fixture).length, 0);
|
|
});
|
|
|
|
test("grouping preserves order and contains only variants that pass all filters", () => {
|
|
const visible = filter({ elements: ["water"] }, fixture);
|
|
const groups = groupAniimoVariants(visible);
|
|
assert.equal(groups.length, 1);
|
|
assert.deepEqual(groups[0].map((v) => v.form), ["water_form", "both_form"]);
|
|
assert.deepEqual(groupAniimoVariants([]), []);
|
|
assert.equal(groupAniimoVariants(data.variants).length, aniimos.length);
|
|
});
|
|
|
|
test("loader passes cancellation to all requests and reports failed responses", async (context) => {
|
|
const calls = [];
|
|
const controller = new AbortController();
|
|
const bodies = [aniimos, fr, en];
|
|
context.mock.method(globalThis, "fetch", async (url, options) => {
|
|
calls.push({ url, options });
|
|
const body = bodies[calls.length - 1];
|
|
return { ok: true, json: async () => body };
|
|
});
|
|
const result = await loadAniimoData(controller.signal);
|
|
assert.equal(result.variants.length, data.variants.length);
|
|
assert.equal(calls.length, 3);
|
|
assert.ok(calls.every((call) => call.options.signal === controller.signal));
|
|
globalThis.fetch.mock.mockImplementation(async () => ({ ok: false }));
|
|
await assert.rejects(loadAniimoData(controller.signal), /indisponibles/);
|
|
});
|