Files
CRAPP/test/unit/scout-store.test.ts
T
davideandClaude Opus 5 af563603f9 Add a test suite for the domain logic and the server routes.
The project had no automated verification at all, so the "Test" step in
the AGENTS.md workflow rested entirely on clicking through the app.

The suite runs on bun with node:assert and adds no dependency: every file
is a script that exits non-zero when a check fails, and test/run.ts runs
each one in its own process. Unit tests cover the rules that decide what
players see (badges, streaks, ball duty rotation, ratings, MVP ties,
scouting totals, goals, notifications, CSI parsing); integration and
end-to-end tests drive the real dev server. Nothing writes to the
database, so both can be pointed at a live environment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 13:07:41 +02:00

121 lines
4.2 KiB
TypeScript

/** Check dello scout: `bun test/unit/scout-store.test.ts`. */
import assert from "node:assert/strict";
import { classifica, giocatori } from "@/lib/crapp-data";
import {
azioniMeta,
classificaConScout,
giocatoriConScout,
totaliPerGiocatore,
totaliSquadra,
type Azione,
type ScoutMatch,
} from "@/lib/scout-store";
const a = (tipo: Azione["tipo"], giocatoreId?: string, set = 1): Azione => ({
id: `${tipo}-${giocatoreId ?? "team"}-${set}-${Math.random()}`,
tipo,
...(giocatoreId ? { giocatoreId } : {}),
set,
ts: 0,
});
const match = (
id: string,
setNostri: number,
setLoro: number,
azioni: Azione[] = [],
): ScoutMatch => ({
id,
data: "2026-09-01",
avversario: "Avversari",
casa: true,
setNostri,
setLoro,
parziali: [],
mvp: "",
azioni,
});
// --- totaliPerGiocatore: ace e muri valgono anche come punto ------------------
const azioni = [
a("attacco", "g1"),
a("ace", "g1"),
a("muro", "g1"),
a("errore", "g1"),
a("attacco", "g2"),
a("punto_avv"),
a("errore_avv"),
];
const totali = totaliPerGiocatore(azioni);
assert.deepEqual(totali.get("g1"), { punti: 3, ace: 1, muri: 1, errori: 1 });
assert.deepEqual(totali.get("g2"), { punti: 1, ace: 0, muri: 0, errori: 0 });
assert.equal(totali.size, 2, "le azioni senza giocatore non creano righe");
assert.equal(totaliPerGiocatore([]).size, 0);
// --- totaliSquadra -----------------------------------------------------------
assert.deepEqual(totaliSquadra([match("m1", 3, 0, azioni), match("m2", 3, 1, azioni)]), {
punti: 8,
ace: 2,
muri: 2,
errori: 2,
});
assert.deepEqual(totaliSquadra([]), { punti: 0, ace: 0, muri: 0, errori: 0 });
// --- giocatoriConScout: presenze e MVP, nessuna statistica offensiva ---------
const base = giocatori.find((g) => g.id === "g1")!;
const conScout = giocatoriConScout([match("m1", 3, 0, azioni)], { m1: base.nome });
const dopo = conScout.find((g) => g.id === "g1")!;
assert.equal(dopo.presenze, base.presenze + 1, "chi ha azioni risulta presente");
assert.equal(dopo.mvp, base.mvp + 1, "l'MVP eletto viene sommato");
assert.equal(dopo.totaliEventi, base.totaliEventi + 1);
const assente = conScout.find((g) => g.id === "g3")!;
const baseAssente = giocatori.find((g) => g.id === "g3")!;
assert.equal(assente.presenze, baseAssente.presenze, "chi non ha azioni non guadagna presenze");
assert.equal(assente.totaliEventi, baseAssente.totaliEventi + 1, "l'evento conta per tutti");
assert.deepEqual(giocatoriConScout([]), giocatori, "senza partite la rosa resta invariata");
// --- classificaConScout: 3 punti netti, 2 al tie-break vinto, 1 al perso -----
assert.deepEqual(classificaConScout([]), classifica, "senza scout resta la classifica di partenza");
const noi = "CRAP Volley";
const rigaBase = classifica.find((r) => r.squadra === noi)!;
const puntiDopo = (m: ScoutMatch) =>
classificaConScout([m]).find((r) => r.squadra === noi)!.punti - rigaBase.punti;
assert.equal(puntiDopo(match("v1", 3, 0)), 3, "3-0 vale 3 punti");
assert.equal(puntiDopo(match("v2", 3, 1)), 3, "3-1 vale 3 punti");
assert.equal(puntiDopo(match("v3", 3, 2)), 2, "3-2 vale 2 punti");
assert.equal(puntiDopo(match("p1", 2, 3)), 1, "2-3 vale 1 punto");
assert.equal(puntiDopo(match("p2", 0, 3)), 0, "0-3 non vale punti");
const aggiornata = classificaConScout([match("v1", 3, 1)]);
const nostra = aggiornata.find((r) => r.squadra === noi)!;
assert.equal(nostra.giocate, rigaBase.giocate + 1);
assert.equal(nostra.vinte, rigaBase.vinte + 1);
assert.equal(nostra.setFatti, rigaBase.setFatti + 3);
assert.equal(nostra.setSubiti, rigaBase.setSubiti + 1);
assert.deepEqual(
aggiornata.map((r) => r.pos),
aggiornata.map((_, i) => i + 1),
"le posizioni vengono rinumerate dopo il riordino",
);
assert.deepEqual(
aggiornata.map((r) => r.punti),
[...aggiornata.map((r) => r.punti)].sort((x, y) => y - x),
"ordinata per punti decrescenti",
);
assert.equal(
aggiornata.filter((r) => r.squadra !== noi).length,
classifica.length - 1,
"le altre squadre restano invariate",
);
// --- metadati azioni ---------------------------------------------------------
for (const [tipo, meta] of Object.entries(azioniMeta)) {
assert.ok(meta.label && meta.short, `${tipo}: etichette presenti`);
}
console.log("scout-store: ok");