Completa la copertura test di tutti i badge e corregge due bug trovati in audit
Audit dedicato su tutti i 16 badge: s-tiebreak non applicava la soglia minima di voti pagella di Pagellone sullo stesso campo mediaVoto (corretto), s-cacche prometteva "partite di campionato" senza che il codice lo verificasse mai (corretta la descrizione, comportamento invariato). Aggiunti test unit e integration end-to-end mancanti su badge segreti e social, con dati scritti a database anche per i cinque segreti che prima ne erano privi. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Badge social (5 categorie) end-to-end contro il database locale:
|
||||
* `bun test/integration/badge-social.test.ts`.
|
||||
*
|
||||
* I test unitari (`test/unit/badge-social.test.ts`) verificano `conteggioCategoria()`,
|
||||
* `vincitoreCategoria()` e `badgeSocialVinti()` come funzioni pure, con categorie inventate
|
||||
* ("sorriso", "urlo" in `scritture.test.ts`) o con solo 2-3 delle 5 reali. Qui invece si
|
||||
* scrivono voti veri su `badge_social_voti` con le 5 categorie effettive di `categorieSocial`
|
||||
* (`affidabile`, `spirito`, `fairplay`, `meme`, `cuore`), si rileggono via REST con la stessa
|
||||
* selezione di `useVotiSocial()`, e si passa il risultato attraverso `badgeSocialVinti()`: se
|
||||
* una colonna cambia nome o un id di categoria diverge da quello scritto dall'app, qui si vede,
|
||||
* perché non c'è nessun vincolo CHECK a database sulla colonna `categoria` (vedi
|
||||
* `docs/modules/badge.md` § Problemi noti) — l'unica difesa è l'app che manda sempre uno dei 5
|
||||
* id validi, e questo test lo dimostra con dati reali su tutte e 5, non solo su un paio.
|
||||
*
|
||||
* Gira solo sullo stack locale (`npx supabase start`) e cancella le proprie righe alla fine:
|
||||
* usa id con il prefisso `test-badge-social`, che nessun dato vero può avere.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
badgeSocialVinti,
|
||||
categorieSocial,
|
||||
vincitoreCategoria,
|
||||
type VotoSocial,
|
||||
} from "@/lib/badge-social";
|
||||
import { statoLocale } from "../helpers/locale";
|
||||
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||
|
||||
const locale = statoLocale();
|
||||
|
||||
if (!locale) {
|
||||
salta("badge social sul database", "stack locale non attivo (npx supabase start)");
|
||||
riepilogo("badge-social");
|
||||
} else {
|
||||
const { url: URL_BASE, servizio: SERVIZIO } = locale;
|
||||
console.log(`badge social su ${URL_BASE}`);
|
||||
|
||||
const PREFISSO = "test-badge-social";
|
||||
|
||||
const rest = (percorso: string, init?: RequestInit) =>
|
||||
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
|
||||
...init,
|
||||
headers: {
|
||||
apikey: SERVIZIO,
|
||||
Authorization: `Bearer ${SERVIZIO}`,
|
||||
"content-type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
async function vota(voto: VotoSocial) {
|
||||
const res = await rest("badge_social_voti?on_conflict=match_id,categoria,votante_id", {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify(voto),
|
||||
});
|
||||
if (!res.ok) throw new Error(`upsert su badge_social_voti: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
async function leggiVoti(): Promise<VotoSocial[]> {
|
||||
const res = await rest(
|
||||
`badge_social_voti?match_id=like.${PREFISSO}-*&select=match_id,categoria,votante_id,votato_id,votato_nome`,
|
||||
);
|
||||
return (await res.json()) as VotoSocial[];
|
||||
}
|
||||
|
||||
try {
|
||||
await prova(
|
||||
"tutte e 5 le categorie reali si contano e si vincono indipendentemente",
|
||||
async () => {
|
||||
// "bs1" vince nettamente tutte e 5 le categorie reali nella partita m1 (2 voti contro
|
||||
// 1 ciascuna): dimostra che l'id di categoria non è solo una stringa di comodo nei
|
||||
// test unitari, ma funziona identico per tutte e 5 quelle vere dell'app.
|
||||
for (const cat of categorieSocial) {
|
||||
await vota({
|
||||
match_id: `${PREFISSO}-m1`,
|
||||
categoria: cat.id,
|
||||
votante_id: "va",
|
||||
votato_id: "bs1",
|
||||
votato_nome: "Uno",
|
||||
});
|
||||
await vota({
|
||||
match_id: `${PREFISSO}-m1`,
|
||||
categoria: cat.id,
|
||||
votante_id: "vb",
|
||||
votato_id: "bs1",
|
||||
votato_nome: "Uno",
|
||||
});
|
||||
await vota({
|
||||
match_id: `${PREFISSO}-m1`,
|
||||
categoria: cat.id,
|
||||
votante_id: "vc",
|
||||
votato_id: "bs2",
|
||||
votato_nome: "Due",
|
||||
});
|
||||
}
|
||||
|
||||
const voti = await leggiVoti();
|
||||
assert.equal(voti.length, categorieSocial.length * 3, "tutti i voti scritti si rileggono");
|
||||
|
||||
for (const cat of categorieSocial) {
|
||||
const vincitore = vincitoreCategoria(voti, `${PREFISSO}-m1`, cat.id);
|
||||
assert.equal(vincitore?.id, "bs1", `bs1 vince "${cat.id}" con vantaggio netto`);
|
||||
}
|
||||
|
||||
const vinti = badgeSocialVinti(voti, "bs1");
|
||||
assert.deepEqual(
|
||||
vinti,
|
||||
Object.fromEntries(categorieSocial.map((c) => [c.id, 1])),
|
||||
"una vittoria per ciascuna delle 5 categorie reali, nessuna persa per strada",
|
||||
);
|
||||
assert.deepEqual(badgeSocialVinti(voti, "bs2"), {}, "bs2 non vince mai nettamente");
|
||||
},
|
||||
);
|
||||
|
||||
await prova("una parità su una categoria reale non assegna il badge, le altre sì", async () => {
|
||||
// Nella partita m2, "affidabile" finisce in parità (nessun vincitore), le altre 4 le
|
||||
// vince ancora "bs1": la parità deve bloccare solo la categoria coinvolta.
|
||||
await vota({
|
||||
match_id: `${PREFISSO}-m2`,
|
||||
categoria: "affidabile",
|
||||
votante_id: "va",
|
||||
votato_id: "bs1",
|
||||
votato_nome: "Uno",
|
||||
});
|
||||
await vota({
|
||||
match_id: `${PREFISSO}-m2`,
|
||||
categoria: "affidabile",
|
||||
votante_id: "vb",
|
||||
votato_id: "bs2",
|
||||
votato_nome: "Due",
|
||||
});
|
||||
for (const cat of categorieSocial.filter((c) => c.id !== "affidabile")) {
|
||||
await vota({
|
||||
match_id: `${PREFISSO}-m2`,
|
||||
categoria: cat.id,
|
||||
votante_id: "va",
|
||||
votato_id: "bs1",
|
||||
votato_nome: "Uno",
|
||||
});
|
||||
}
|
||||
|
||||
const voti = await leggiVoti();
|
||||
assert.equal(
|
||||
vincitoreCategoria(voti, `${PREFISSO}-m2`, "affidabile"),
|
||||
null,
|
||||
"1 voto contro 1: parità, nessun vincitore",
|
||||
);
|
||||
const vintiTotali = badgeSocialVinti(voti, "bs1");
|
||||
// m1 (tutte e 5) + m2 (le 4 non in parità): "affidabile" resta a 1 (solo m1), le altre 4 a 2.
|
||||
assert.equal(vintiTotali["affidabile"], 1, "la parità in m2 non aggiunge una vittoria");
|
||||
for (const cat of categorieSocial.filter((c) => c.id !== "affidabile")) {
|
||||
assert.equal(vintiTotali[cat.id], 2, `"${cat.id}" vinta sia in m1 sia in m2`);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
await rest(`badge_social_voti?match_id=like.${PREFISSO}-*`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
riepilogo("badge-social");
|
||||
}
|
||||
@@ -117,8 +117,23 @@ if (!locale) {
|
||||
[`${PREFISSO}-m7`, "2020-01-07", "pv1"],
|
||||
] as const;
|
||||
|
||||
for (const [id, data] of eventi) await creaEvento(id, data);
|
||||
for (const [id, , giocatore] of eventi) if (giocatore) await confermaTurno(id, giocatore);
|
||||
for (const [id, data] of eventi.slice(0, 3)) await creaEvento(id, data);
|
||||
for (const [id, , giocatore] of eventi.slice(0, 3))
|
||||
if (giocatore) await confermaTurno(id, giocatore);
|
||||
|
||||
const eventiA3 = await leggiEventi();
|
||||
const salvatiA3 = await leggiTurniSalvati();
|
||||
const conteggioA3 = conteggioTurni(salvatiA3, eventiA3, OGGI);
|
||||
assert.equal(conteggioA3["pv1"], 3, "3 turni confermati: soglia bronzo appena raggiunta");
|
||||
assert.equal(
|
||||
statoBadge(palloniDef, giocatoreAzzerato(conteggioA3["pv1"]!)).grado,
|
||||
"bronzo",
|
||||
"3 turni: bronzo, non oltre (soglia argento è 6)",
|
||||
);
|
||||
|
||||
for (const [id, data] of eventi.slice(3)) await creaEvento(id, data);
|
||||
for (const [id, , giocatore] of eventi.slice(3))
|
||||
if (giocatore) await confermaTurno(id, giocatore);
|
||||
|
||||
const eventiLetti = await leggiEventi();
|
||||
assert.equal(eventiLetti.length, 7, "tutti gli eventi scritti si rileggono");
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Badge segreto Trono di ferro end-to-end contro il database locale:
|
||||
* `bun test/integration/s-cacche-badge.test.ts`.
|
||||
*
|
||||
* I test unitari (`test/unit/cacche.test.ts`, `test/unit/badges.test.ts`) verificano
|
||||
* `statisticheCacche()` e `statoBadge()` come funzioni pure. Qui invece si scrivono righe vere
|
||||
* su `cacche_partita`, si rileggono via REST con la stessa selezione di `useCacche()`, e si
|
||||
* passa il risultato attraverso `statisticheCacche()` fino a `statoBadge()` sul badge segreto
|
||||
* `s-cacche`.
|
||||
*
|
||||
* Copre in particolare un comportamento verificato in analisi e confermato intenzionale (non
|
||||
* un bug): **la descrizione del badge non distingue più campionato da amichevoli** ("almeno 3
|
||||
* partite affrontate con 3+ cacche pre-gara, campionato o amichevole") perché nessuna funzione
|
||||
* della pipeline (`statisticheCacche()`, `rosa.ts`, `badges.ts`) filtra mai su
|
||||
* `eventi_app.campionato` — la vecchia descrizione prometteva "partite di campionato" senza
|
||||
* che il codice lo verificasse mai. Qui si scrive apposta un evento con `campionato=false`
|
||||
* (amichevole) e si dimostra che conta lo stesso, così un domani chi reintroduce un filtro sul
|
||||
* campionato deve toccare anche questo test, non scoprirlo in produzione.
|
||||
*
|
||||
* Gira solo sullo stack locale (`npx supabase start`) e cancella le proprie righe alla fine:
|
||||
* usa id con il prefisso `test-s-cacche-badge`, che nessun dato vero può avere.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { badgeSegreti, statoBadge } from "@/lib/badges";
|
||||
import { statisticheCacche, type RigaCacche } from "@/lib/cacche";
|
||||
import { giocatori, type Giocatore } from "@/lib/crapp-data";
|
||||
import { statoLocale } from "../helpers/locale";
|
||||
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||
|
||||
const locale = statoLocale();
|
||||
|
||||
if (!locale) {
|
||||
salta("badge Trono di ferro sul database", "stack locale non attivo (npx supabase start)");
|
||||
riepilogo("s-cacche-badge");
|
||||
} else {
|
||||
const { url: URL_BASE, servizio: SERVIZIO } = locale;
|
||||
console.log(`badge Trono di ferro su ${URL_BASE}`);
|
||||
|
||||
const PREFISSO = "test-s-cacche-badge";
|
||||
const def = badgeSegreti.find((b) => b.id === "s-cacche")!;
|
||||
|
||||
const rest = (percorso: string, init?: RequestInit) =>
|
||||
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
|
||||
...init,
|
||||
headers: {
|
||||
apikey: SERVIZIO,
|
||||
Authorization: `Bearer ${SERVIZIO}`,
|
||||
"content-type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
async function creaEvento(id: string, campionato: boolean) {
|
||||
const res = await rest("eventi_app", {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
tipo: "partita",
|
||||
titolo: `Partita ${id}`,
|
||||
data: "2020-01-01",
|
||||
campionato,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(`creazione evento ${id}: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
async function dichiara(eventoId: string, giocatoreId: string, quantita: number) {
|
||||
const res = await rest("cacche_partita?on_conflict=evento_id,giocatore_id", {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify({ evento_id: eventoId, giocatore_id: giocatoreId, quantita }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`upsert su cacche_partita: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
async function leggiCacche(): Promise<RigaCacche[]> {
|
||||
const res = await rest(
|
||||
`cacche_partita?evento_id=like.${PREFISSO}*&select=evento_id,giocatore_id,quantita`,
|
||||
);
|
||||
return (await res.json()) as RigaCacche[];
|
||||
}
|
||||
|
||||
function giocatoreAzzerato(cacche: number): Giocatore {
|
||||
return {
|
||||
...giocatori[0]!,
|
||||
cacche,
|
||||
mvp: 0,
|
||||
mediaVoto: 0,
|
||||
votiPagella: 0,
|
||||
palloni: 0,
|
||||
presenze: 0,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await prova(
|
||||
"il segreto si sblocca con 3 giornate da record, comprese le amichevoli",
|
||||
async () => {
|
||||
// sc1: 2 giornate top su partite di campionato, 1 su un'amichevole. Il badge non fa
|
||||
// distinzione: le 3 contano tutte allo stesso modo.
|
||||
await creaEvento(`${PREFISSO}-c1`, true);
|
||||
await creaEvento(`${PREFISSO}-c2`, true);
|
||||
await creaEvento(`${PREFISSO}-a1`, false); // amichevole
|
||||
await dichiara(`${PREFISSO}-c1`, "sc1", 3);
|
||||
await dichiara(`${PREFISSO}-c2`, "sc1", 4);
|
||||
|
||||
const righeA2 = await leggiCacche();
|
||||
const statsA2 = statisticheCacche(righeA2);
|
||||
assert.equal(statsA2["sc1"]?.giornateTop, 2, "solo le due di campionato per ora");
|
||||
assert.equal(
|
||||
statoBadge(def, giocatoreAzzerato(statsA2["sc1"]!.giornateTop)).grado,
|
||||
null,
|
||||
"2 giornate top non bastano",
|
||||
);
|
||||
|
||||
await dichiara(`${PREFISSO}-a1`, "sc1", 3);
|
||||
const righe = await leggiCacche();
|
||||
const stats = statisticheCacche(righe);
|
||||
assert.equal(stats["sc1"]?.giornateTop, 3, "l'amichevole conta come le altre due");
|
||||
assert.equal(
|
||||
statoBadge(def, giocatoreAzzerato(stats["sc1"]!.giornateTop)).grado,
|
||||
"oro",
|
||||
"3 giornate top, campionato o amichevole: il segreto si sblocca",
|
||||
);
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
await rest(`cacche_partita?evento_id=like.${PREFISSO}*`, { method: "DELETE" });
|
||||
await rest(`eventi_app?id=like.${PREFISSO}*`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
riepilogo("s-cacche-badge");
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Badge segreto Cliente VIP dell'Infermeria end-to-end contro il database locale:
|
||||
* `bun test/integration/s-infermeria-badge.test.ts`.
|
||||
*
|
||||
* I test unitari (`test/unit/infortuni.test.ts`, `test/unit/badges.test.ts`) verificano
|
||||
* `contaInfortuni()` e `statoBadge()` come funzioni pure, con eventi e risposte costruiti a
|
||||
* mano. Qui invece si scrivono eventi e risposte "infortunato" veri su `eventi_app`/
|
||||
* `risposte_presenze`, si rileggono via REST con la stessa forma di `daRiga()`/
|
||||
* `fetchPresenze()`, e si passa il risultato attraverso `contaInfortuni()` fino a
|
||||
* `statoBadge()` sul badge segreto `s-infermeria`: se una colonna cambia nome o la mappatura
|
||||
* si rompe, qui il badge torna sbagliato anche se i test unitari restano verdi.
|
||||
*
|
||||
* Gira solo sullo stack locale (`npx supabase start`) e cancella le proprie righe alla fine:
|
||||
* usa id con il prefisso `test-s-infermeria-badge`, che nessun dato vero può avere.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { badgeSegreti, statoBadge } from "@/lib/badges";
|
||||
import { contaInfortuni } from "@/lib/infortuni";
|
||||
import { daRiga, type RigaEvento, type Evento } from "@/lib/eventi";
|
||||
import { giocatori, type Giocatore } from "@/lib/crapp-data";
|
||||
import { statoLocale } from "../helpers/locale";
|
||||
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||
|
||||
const locale = statoLocale();
|
||||
|
||||
if (!locale) {
|
||||
salta(
|
||||
"badge Cliente VIP dell'Infermeria sul database",
|
||||
"stack locale non attivo (npx supabase start)",
|
||||
);
|
||||
riepilogo("s-infermeria-badge");
|
||||
} else {
|
||||
const { url: URL_BASE, servizio: SERVIZIO } = locale;
|
||||
console.log(`badge Cliente VIP dell'Infermeria su ${URL_BASE}`);
|
||||
|
||||
const PREFISSO = "test-s-infermeria-badge";
|
||||
const def = badgeSegreti.find((b) => b.id === "s-infermeria")!;
|
||||
const OGGI = "2099-01-01";
|
||||
|
||||
const rest = (percorso: string, init?: RequestInit) =>
|
||||
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
|
||||
...init,
|
||||
headers: {
|
||||
apikey: SERVIZIO,
|
||||
Authorization: `Bearer ${SERVIZIO}`,
|
||||
"content-type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
const dataEvento = (i: number) => {
|
||||
const d = new Date(Date.UTC(2020, 0, 1));
|
||||
d.setUTCDate(d.getUTCDate() + i - 1);
|
||||
return d.toISOString().slice(0, 10);
|
||||
};
|
||||
|
||||
async function creaEvento(id: string, i: number) {
|
||||
const res = await rest("eventi_app", {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
tipo: "allenamento",
|
||||
titolo: `Evento ${id}`,
|
||||
data: dataEvento(i),
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(`creazione evento ${id}: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
async function rispondi(eventoId: string, giocatoreId: string, stato: string) {
|
||||
const res = await rest("risposte_presenze?on_conflict=evento_id,giocatore_id", {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify({ evento_id: eventoId, giocatore_id: giocatoreId, stato }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`upsert su risposte_presenze: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
async function leggiEventi(): Promise<Evento[]> {
|
||||
const res = await rest(
|
||||
"eventi_app?id=like." +
|
||||
PREFISSO +
|
||||
"*&select=id,tipo,titolo,luogo,data,ora,note,convocati,campionato,casa,pagelle_chiuse,creato_il",
|
||||
);
|
||||
const righe = (await res.json()) as RigaEvento[];
|
||||
return righe.map(daRiga);
|
||||
}
|
||||
|
||||
async function leggiPresenze(): Promise<Record<string, Record<string, string>>> {
|
||||
const res = await rest(
|
||||
`risposte_presenze?evento_id=like.${PREFISSO}*&select=evento_id,giocatore_id,stato`,
|
||||
);
|
||||
const righe = (await res.json()) as Array<{
|
||||
evento_id: string;
|
||||
giocatore_id: string;
|
||||
stato: string;
|
||||
}>;
|
||||
const mappa: Record<string, Record<string, string>> = {};
|
||||
for (const r of righe) (mappa[r.evento_id] ??= {})[r.giocatore_id] = r.stato;
|
||||
return mappa;
|
||||
}
|
||||
|
||||
function giocatoreAzzerato(infortuni: number): Giocatore {
|
||||
return {
|
||||
...giocatori[0]!,
|
||||
infortuni,
|
||||
mvp: 0,
|
||||
mediaVoto: 0,
|
||||
votiPagella: 0,
|
||||
palloni: 0,
|
||||
presenze: 0,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await prova("il segreto si sblocca al terzo infortunio vero, non prima", async () => {
|
||||
for (let i = 1; i <= 3; i += 1) {
|
||||
const id = `${PREFISSO}-e${i}`;
|
||||
await creaEvento(id, i);
|
||||
await rispondi(id, "si1", "infortunato");
|
||||
}
|
||||
|
||||
const eventiA2 = (await leggiEventi()).filter((e) => e.id !== `${PREFISSO}-e3`);
|
||||
const presenzeA2 = await leggiPresenze();
|
||||
const contoA2 = contaInfortuni(presenzeA2, eventiA2, OGGI)["si1"] ?? 0;
|
||||
assert.equal(contoA2, 2, "solo i primi due eventi contati");
|
||||
assert.equal(
|
||||
statoBadge(def, giocatoreAzzerato(contoA2)).grado,
|
||||
null,
|
||||
"2 infortuni non bastano",
|
||||
);
|
||||
|
||||
const eventi = await leggiEventi();
|
||||
const presenze = await leggiPresenze();
|
||||
const conto = contaInfortuni(presenze, eventi, OGGI)["si1"] ?? 0;
|
||||
assert.equal(conto, 3);
|
||||
assert.equal(
|
||||
statoBadge(def, giocatoreAzzerato(conto)).grado,
|
||||
"oro",
|
||||
"3 infortuni: il segreto si sblocca (soglia unica)",
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
await rest(`risposte_presenze?evento_id=like.${PREFISSO}*`, { method: "DELETE" });
|
||||
await rest(`eventi_app?id=like.${PREFISSO}*`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
riepilogo("s-infermeria-badge");
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* Badge segreto Mai un forfait end-to-end contro il database locale:
|
||||
* `bun test/integration/s-mai-forfait-badge.test.ts`.
|
||||
*
|
||||
* I test unitari (`test/unit/presenze.test.ts`, `test/unit/badges.test.ts`) verificano
|
||||
* `serieConferme()`, `contaPresenzeGiocatore()` e `statoBadge()` come funzioni pure. Qui invece
|
||||
* si scrivono eventi con `creato_il` esplicito e risposte con `risposto_il` esplicito su
|
||||
* `eventi_app`/`risposte_presenze`, si rileggono via REST e si passa il risultato attraverso
|
||||
* entrambe le funzioni fino a `statoBadge()` sul badge segreto `s-mai-forfait`, che è l'unico a
|
||||
* combinare due statistiche indipendenti (serie di conferme rapide **e** presenze totali).
|
||||
*
|
||||
* Gira solo sullo stack locale (`npx supabase start`) e cancella le proprie righe alla fine:
|
||||
* usa id con il prefisso `test-s-mai-forfait-badge`, che nessun dato vero può avere.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { badgeSegreti, statoBadge } from "@/lib/badges";
|
||||
import { serieConferme, contaPresenzeGiocatore } from "@/lib/presenze";
|
||||
import { daRiga, type RigaEvento, type Evento } from "@/lib/eventi";
|
||||
import { giocatori, type Giocatore } from "@/lib/crapp-data";
|
||||
import { statoLocale } from "../helpers/locale";
|
||||
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||
|
||||
const locale = statoLocale();
|
||||
|
||||
if (!locale) {
|
||||
salta("badge Mai un forfait sul database", "stack locale non attivo (npx supabase start)");
|
||||
riepilogo("s-mai-forfait-badge");
|
||||
} else {
|
||||
const { url: URL_BASE, servizio: SERVIZIO } = locale;
|
||||
console.log(`badge Mai un forfait su ${URL_BASE}`);
|
||||
|
||||
const PREFISSO = "test-s-mai-forfait-badge";
|
||||
const def = badgeSegreti.find((b) => b.id === "s-mai-forfait")!;
|
||||
const OGGI = "2099-01-01";
|
||||
|
||||
const rest = (percorso: string, init?: RequestInit) =>
|
||||
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
|
||||
...init,
|
||||
headers: {
|
||||
apikey: SERVIZIO,
|
||||
Authorization: `Bearer ${SERVIZIO}`,
|
||||
"content-type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
const dataEvento = (i: number) => {
|
||||
const d = new Date(Date.UTC(2020, 0, 1));
|
||||
d.setUTCDate(d.getUTCDate() + i - 1);
|
||||
return d.toISOString().slice(0, 10);
|
||||
};
|
||||
|
||||
async function creaEvento(id: string, i: number) {
|
||||
const res = await rest("eventi_app", {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
tipo: "allenamento",
|
||||
titolo: `Evento ${id}`,
|
||||
data: dataEvento(i),
|
||||
creato_il: `${dataEvento(i)}T08:00:00Z`,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(`creazione evento ${id}: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
async function rispondi(eventoId: string, giocatoreId: string, i: number) {
|
||||
const res = await rest("risposte_presenze?on_conflict=evento_id,giocatore_id", {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify({
|
||||
evento_id: eventoId,
|
||||
giocatore_id: giocatoreId,
|
||||
stato: "presente",
|
||||
risposto_il: `${dataEvento(i)}T08:30:00Z`,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(`upsert su risposte_presenze: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
async function leggiEventi(): Promise<Evento[]> {
|
||||
const res = await rest(
|
||||
"eventi_app?id=like." +
|
||||
PREFISSO +
|
||||
"*&select=id,tipo,titolo,luogo,data,ora,note,convocati,campionato,casa,pagelle_chiuse,creato_il",
|
||||
);
|
||||
const righe = (await res.json()) as RigaEvento[];
|
||||
return righe.map(daRiga);
|
||||
}
|
||||
|
||||
async function leggiPresenzeETempi(): Promise<{
|
||||
presenze: Record<string, Record<string, string>>;
|
||||
tempi: Record<string, Record<string, string>>;
|
||||
}> {
|
||||
const res = await rest(
|
||||
`risposte_presenze?evento_id=like.${PREFISSO}*&select=evento_id,giocatore_id,stato,risposto_il`,
|
||||
);
|
||||
const righe = (await res.json()) as Array<{
|
||||
evento_id: string;
|
||||
giocatore_id: string;
|
||||
stato: string;
|
||||
risposto_il: string;
|
||||
}>;
|
||||
const presenze: Record<string, Record<string, string>> = {};
|
||||
const tempi: Record<string, Record<string, string>> = {};
|
||||
for (const r of righe) {
|
||||
(presenze[r.evento_id] ??= {})[r.giocatore_id] = r.stato;
|
||||
(tempi[r.evento_id] ??= {})[r.giocatore_id] = r.risposto_il;
|
||||
}
|
||||
return { presenze, tempi };
|
||||
}
|
||||
|
||||
function giocatoreAzzerato(serieConferme: number, presenze: number): Giocatore {
|
||||
return {
|
||||
...giocatori[0]!,
|
||||
serieConferme,
|
||||
presenze,
|
||||
mvp: 0,
|
||||
mediaVoto: 0,
|
||||
votiPagella: 0,
|
||||
palloni: 0,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await prova(
|
||||
"il segreto si sblocca solo quando entrambe le condizioni sono vere insieme",
|
||||
async () => {
|
||||
// "mf1": 15 eventi passati, presente e confermato in fretta a tutti — soddisfa
|
||||
// ampiamente sia la serie di conferme (>=10) sia le presenze (>=15).
|
||||
for (let i = 1; i <= 15; i += 1) {
|
||||
const id = `${PREFISSO}-a${String(i).padStart(2, "0")}`;
|
||||
await creaEvento(id, i);
|
||||
await rispondi(id, "mf1", i);
|
||||
}
|
||||
|
||||
const eventi15 = await leggiEventi();
|
||||
const { presenze: p15, tempi: t15 } = await leggiPresenzeETempi();
|
||||
|
||||
// Solo 9 eventi: serie di conferme appena sotto soglia, presenze sotto soglia.
|
||||
const eventi9 = eventi15.slice(0, 9);
|
||||
const serie9 = serieConferme("mf1", eventi9, t15, OGGI);
|
||||
const pres9 = contaPresenzeGiocatore("mf1", eventi9, p15, OGGI);
|
||||
assert.equal(serie9, 9);
|
||||
assert.equal(pres9, 9);
|
||||
assert.equal(
|
||||
statoBadge(def, giocatoreAzzerato(serie9, pres9)).grado,
|
||||
null,
|
||||
"9 conferme e 9 presenze: nessuna delle due soglie raggiunta",
|
||||
);
|
||||
|
||||
const serie15 = serieConferme("mf1", eventi15, t15, OGGI);
|
||||
const pres15 = contaPresenzeGiocatore("mf1", eventi15, p15, OGGI);
|
||||
assert.equal(serie15, 15);
|
||||
assert.equal(pres15, 15);
|
||||
assert.equal(
|
||||
statoBadge(def, giocatoreAzzerato(serie15, pres15)).grado,
|
||||
"oro",
|
||||
"serie conferme >=10 e presenze >=15: il segreto si sblocca",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await prova("una risposta lenta rompe la serie ma non le presenze già accumulate", async () => {
|
||||
// Un sedicesimo evento con risposta arrivata oltre le 24h: la serie conferme torna a 0,
|
||||
// ma le presenze (indipendenti) restano a 16 — il segreto deve richiudersi, non serve
|
||||
// ripartire da zero anche sulle presenze.
|
||||
const idLento = `${PREFISSO}-a16`;
|
||||
await creaEvento(idLento, 16);
|
||||
const res = await rest("risposte_presenze?on_conflict=evento_id,giocatore_id", {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify({
|
||||
evento_id: idLento,
|
||||
giocatore_id: "mf1",
|
||||
stato: "presente",
|
||||
risposto_il: `${dataEvento(18)}T08:00:00Z`, // 2 giorni dopo la convocazione
|
||||
}),
|
||||
});
|
||||
if (!res.ok)
|
||||
throw new Error(`upsert su risposte_presenze: ${res.status} ${await res.text()}`);
|
||||
|
||||
const eventi = await leggiEventi();
|
||||
const { presenze, tempi } = await leggiPresenzeETempi();
|
||||
const serie = serieConferme("mf1", eventi, tempi, OGGI);
|
||||
const conto = contaPresenzeGiocatore("mf1", eventi, presenze, OGGI);
|
||||
assert.equal(serie, 0, "la risposta lenta azzera la serie di conferme");
|
||||
assert.equal(conto, 16, "le presenze restano quelle di sempre, indipendenti dalla serie");
|
||||
assert.equal(
|
||||
statoBadge(def, giocatoreAzzerato(serie, conto)).grado,
|
||||
null,
|
||||
"presenze abbondanti ma serie azzerata: il segreto si richiude",
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
await rest(`risposte_presenze?evento_id=like.${PREFISSO}*`, { method: "DELETE" });
|
||||
await rest(`eventi_app?id=like.${PREFISSO}*`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
riepilogo("s-mai-forfait-badge");
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Badge segreto Aspettate, arrivo! end-to-end contro il database locale:
|
||||
* `bun test/integration/s-ritardi-badge.test.ts`.
|
||||
*
|
||||
* Stessa struttura di `s-infermeria-badge.test.ts` ma sullo stato "ritardo" invece di
|
||||
* "infortunato": scrive eventi e risposte veri su `eventi_app`/`risposte_presenze`, rilegge
|
||||
* via REST e passa il risultato attraverso `contaRitardi()` fino a `statoBadge()` sul badge
|
||||
* segreto `s-ritardi` (soglia 5, unica).
|
||||
*
|
||||
* Gira solo sullo stack locale (`npx supabase start`) e cancella le proprie righe alla fine:
|
||||
* usa id con il prefisso `test-s-ritardi-badge`, che nessun dato vero può avere.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { badgeSegreti, statoBadge } from "@/lib/badges";
|
||||
import { contaRitardi } from "@/lib/infortuni";
|
||||
import { daRiga, type RigaEvento, type Evento } from "@/lib/eventi";
|
||||
import { giocatori, type Giocatore } from "@/lib/crapp-data";
|
||||
import { statoLocale } from "../helpers/locale";
|
||||
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||
|
||||
const locale = statoLocale();
|
||||
|
||||
if (!locale) {
|
||||
salta("badge Aspettate, arrivo! sul database", "stack locale non attivo (npx supabase start)");
|
||||
riepilogo("s-ritardi-badge");
|
||||
} else {
|
||||
const { url: URL_BASE, servizio: SERVIZIO } = locale;
|
||||
console.log(`badge Aspettate, arrivo! su ${URL_BASE}`);
|
||||
|
||||
const PREFISSO = "test-s-ritardi-badge";
|
||||
const def = badgeSegreti.find((b) => b.id === "s-ritardi")!;
|
||||
const OGGI = "2099-01-01";
|
||||
|
||||
const rest = (percorso: string, init?: RequestInit) =>
|
||||
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
|
||||
...init,
|
||||
headers: {
|
||||
apikey: SERVIZIO,
|
||||
Authorization: `Bearer ${SERVIZIO}`,
|
||||
"content-type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
const dataEvento = (i: number) => {
|
||||
const d = new Date(Date.UTC(2020, 0, 1));
|
||||
d.setUTCDate(d.getUTCDate() + i - 1);
|
||||
return d.toISOString().slice(0, 10);
|
||||
};
|
||||
|
||||
async function creaEvento(id: string, i: number) {
|
||||
const res = await rest("eventi_app", {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
tipo: "allenamento",
|
||||
titolo: `Evento ${id}`,
|
||||
data: dataEvento(i),
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(`creazione evento ${id}: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
async function rispondi(eventoId: string, giocatoreId: string, stato: string) {
|
||||
const res = await rest("risposte_presenze?on_conflict=evento_id,giocatore_id", {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify({ evento_id: eventoId, giocatore_id: giocatoreId, stato }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`upsert su risposte_presenze: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
async function leggiEventi(): Promise<Evento[]> {
|
||||
const res = await rest(
|
||||
"eventi_app?id=like." +
|
||||
PREFISSO +
|
||||
"*&select=id,tipo,titolo,luogo,data,ora,note,convocati,campionato,casa,pagelle_chiuse,creato_il",
|
||||
);
|
||||
const righe = (await res.json()) as RigaEvento[];
|
||||
return righe.map(daRiga);
|
||||
}
|
||||
|
||||
async function leggiPresenze(): Promise<Record<string, Record<string, string>>> {
|
||||
const res = await rest(
|
||||
`risposte_presenze?evento_id=like.${PREFISSO}*&select=evento_id,giocatore_id,stato`,
|
||||
);
|
||||
const righe = (await res.json()) as Array<{
|
||||
evento_id: string;
|
||||
giocatore_id: string;
|
||||
stato: string;
|
||||
}>;
|
||||
const mappa: Record<string, Record<string, string>> = {};
|
||||
for (const r of righe) (mappa[r.evento_id] ??= {})[r.giocatore_id] = r.stato;
|
||||
return mappa;
|
||||
}
|
||||
|
||||
function giocatoreAzzerato(ritardi: number): Giocatore {
|
||||
return {
|
||||
...giocatori[0]!,
|
||||
ritardi,
|
||||
mvp: 0,
|
||||
mediaVoto: 0,
|
||||
votiPagella: 0,
|
||||
palloni: 0,
|
||||
presenze: 0,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await prova("il segreto si sblocca al quinto ritardo vero, non prima", async () => {
|
||||
for (let i = 1; i <= 5; i += 1) {
|
||||
const id = `${PREFISSO}-e${i}`;
|
||||
await creaEvento(id, i);
|
||||
await rispondi(id, "sr1", "ritardo");
|
||||
}
|
||||
|
||||
const eventiA4 = (await leggiEventi()).filter((e) => e.id !== `${PREFISSO}-e5`);
|
||||
const presenzeA4 = await leggiPresenze();
|
||||
const contoA4 = contaRitardi(presenzeA4, eventiA4, OGGI)["sr1"] ?? 0;
|
||||
assert.equal(contoA4, 4, "solo i primi quattro eventi contati");
|
||||
assert.equal(
|
||||
statoBadge(def, giocatoreAzzerato(contoA4)).grado,
|
||||
null,
|
||||
"4 ritardi non bastano",
|
||||
);
|
||||
|
||||
const eventi = await leggiEventi();
|
||||
const presenze = await leggiPresenze();
|
||||
const conto = contaRitardi(presenze, eventi, OGGI)["sr1"] ?? 0;
|
||||
assert.equal(conto, 5);
|
||||
assert.equal(
|
||||
statoBadge(def, giocatoreAzzerato(conto)).grado,
|
||||
"oro",
|
||||
"5 ritardi: il segreto si sblocca (soglia unica)",
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
await rest(`risposte_presenze?evento_id=like.${PREFISSO}*`, { method: "DELETE" });
|
||||
await rest(`eventi_app?id=like.${PREFISSO}*`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
riepilogo("s-ritardi-badge");
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Badge segreto Uomo tie-break end-to-end contro il database locale:
|
||||
* `bun test/integration/s-tiebreak-badge.test.ts`.
|
||||
*
|
||||
* I test unitari (`test/unit/mvp-voti.test.ts`, `test/unit/pagelle.test.ts`,
|
||||
* `test/unit/badges.test.ts`) verificano `mvpVintiPerGiocatore()`, `mediePagelle()` e
|
||||
* `statoBadge()` come funzioni pure. Qui invece si scrivono voti veri su `mvp_voti` e
|
||||
* `pagelle_voti`, si rileggono via REST e si passa il risultato attraverso
|
||||
* `mvpVintiPerGiocatore()`/`mediePagelle()` fino a `statoBadge()` sul badge segreto
|
||||
* `s-tiebreak`.
|
||||
*
|
||||
* Copre in particolare il fix di questa sessione: prima `s-tiebreak` usava `g.mediaVoto` senza
|
||||
* applicare `VOTI_MINIMI_PAGELLA`, a differenza del badge normale Pagellone che usa lo stesso
|
||||
* campo — un solo voto pagella altissimo poteva sbloccare il segreto insieme a 2 MVP, senza
|
||||
* significatività statistica. Qui si dimostra con dati reali che ora serve lo stesso minimo di
|
||||
* voti di Pagellone anche per questo segreto.
|
||||
*
|
||||
* Gira solo sullo stack locale (`npx supabase start`) e cancella le proprie righe alla fine:
|
||||
* usa id con il prefisso `test-s-tiebreak-badge`, che nessun dato vero può avere.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { badgeSegreti, statoBadge, VOTI_MINIMI_PAGELLA } from "@/lib/badges";
|
||||
import { mvpVintiPerGiocatore, type VotoMvp } from "@/lib/mvp-voti";
|
||||
import { mediePagelle, type VotoPagella } from "@/lib/pagelle";
|
||||
import { giocatori, type Giocatore } from "@/lib/crapp-data";
|
||||
import { statoLocale } from "../helpers/locale";
|
||||
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||
|
||||
const locale = statoLocale();
|
||||
|
||||
if (!locale) {
|
||||
salta("badge Uomo tie-break sul database", "stack locale non attivo (npx supabase start)");
|
||||
riepilogo("s-tiebreak-badge");
|
||||
} else {
|
||||
const { url: URL_BASE, servizio: SERVIZIO } = locale;
|
||||
console.log(`badge Uomo tie-break su ${URL_BASE}`);
|
||||
|
||||
const PREFISSO = "test-s-tiebreak-badge";
|
||||
const def = badgeSegreti.find((b) => b.id === "s-tiebreak")!;
|
||||
|
||||
const rest = (percorso: string, init?: RequestInit) =>
|
||||
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
|
||||
...init,
|
||||
headers: {
|
||||
apikey: SERVIZIO,
|
||||
Authorization: `Bearer ${SERVIZIO}`,
|
||||
"content-type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
async function votaMvp(riga: VotoMvp) {
|
||||
const res = await rest("mvp_voti?on_conflict=match_id,votante_id", {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify(riga),
|
||||
});
|
||||
if (!res.ok) throw new Error(`upsert su mvp_voti: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
async function votaPagella(riga: VotoPagella) {
|
||||
const res = await rest("pagelle_voti?on_conflict=match_id,votante_id,votato_id", {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify(riga),
|
||||
});
|
||||
if (!res.ok) throw new Error(`upsert su pagelle_voti: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
async function leggiMvp(): Promise<VotoMvp[]> {
|
||||
const res = await rest(
|
||||
`mvp_voti?match_id=like.${PREFISSO}-*&select=match_id,votante_id,votato_id,votato_nome`,
|
||||
);
|
||||
return (await res.json()) as VotoMvp[];
|
||||
}
|
||||
|
||||
async function leggiPagelle(): Promise<VotoPagella[]> {
|
||||
const res = await rest(
|
||||
`pagelle_voti?match_id=like.${PREFISSO}-*&select=match_id,votante_id,votato_id,voto`,
|
||||
);
|
||||
return (await res.json()) as VotoPagella[];
|
||||
}
|
||||
|
||||
function giocatoreAzzerato(mvp: number, mediaVoto: number, votiPagella: number): Giocatore {
|
||||
return { ...giocatori[0]!, mvp, mediaVoto, votiPagella, palloni: 0, presenze: 0 };
|
||||
}
|
||||
|
||||
try {
|
||||
await prova(
|
||||
"sotto la soglia minima di voti pagella, 2 MVP e media alta non bastano",
|
||||
async () => {
|
||||
// "tb1" vince nettamente m1 e m2 (2 MVP), e riceve un solo voto pagella da 9 (media
|
||||
// alta ma su un campione troppo piccolo): il segreto deve restare bloccato.
|
||||
await votaMvp({
|
||||
match_id: `${PREFISSO}-m1`,
|
||||
votante_id: "va",
|
||||
votato_id: "tb1",
|
||||
votato_nome: "Uno",
|
||||
});
|
||||
await votaMvp({
|
||||
match_id: `${PREFISSO}-m2`,
|
||||
votante_id: "va",
|
||||
votato_id: "tb1",
|
||||
votato_nome: "Uno",
|
||||
});
|
||||
await votaPagella({
|
||||
match_id: `${PREFISSO}-m1`,
|
||||
votante_id: "va",
|
||||
votato_id: "tb1",
|
||||
voto: 9,
|
||||
});
|
||||
|
||||
const vinti = mvpVintiPerGiocatore(await leggiMvp());
|
||||
const medie = mediePagelle(await leggiPagelle());
|
||||
assert.equal(vinti["tb1"], 2, "2 MVP netti");
|
||||
assert.equal(medie["tb1"]?.voti, 1, "un solo voto pagella");
|
||||
assert.equal(medie["tb1"]?.media, 9);
|
||||
|
||||
const badge = statoBadge(
|
||||
def,
|
||||
giocatoreAzzerato(vinti["tb1"]!, medie["tb1"]!.media, medie["tb1"]!.voti),
|
||||
);
|
||||
assert.equal(
|
||||
badge.grado,
|
||||
null,
|
||||
`sotto ${VOTI_MINIMI_PAGELLA} voti: il segreto resta bloccato`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await prova(`al ${VOTI_MINIMI_PAGELLA}° voto pagella il segreto si sblocca`, async () => {
|
||||
// Altri 4 voti pagella allo stesso "tb1", sempre alti: raggiunta la soglia minima, con
|
||||
// 2 MVP e media alta il segreto si sblocca.
|
||||
for (const [i, votante] of ["vb", "vc", "vd", "ve"].entries()) {
|
||||
await votaPagella({
|
||||
match_id: `${PREFISSO}-n${i + 1}`,
|
||||
votante_id: votante,
|
||||
votato_id: "tb1",
|
||||
voto: 9,
|
||||
});
|
||||
}
|
||||
|
||||
const vinti = mvpVintiPerGiocatore(await leggiMvp());
|
||||
const medie = mediePagelle(await leggiPagelle());
|
||||
assert.equal(medie["tb1"]?.voti, 5);
|
||||
assert.equal(medie["tb1"]?.media, 9);
|
||||
|
||||
const badge = statoBadge(
|
||||
def,
|
||||
giocatoreAzzerato(vinti["tb1"]!, medie["tb1"]!.media, medie["tb1"]!.voti),
|
||||
);
|
||||
assert.equal(badge.grado, "oro", "2 MVP, 5 voti, media 9: il segreto si sblocca");
|
||||
});
|
||||
} finally {
|
||||
await rest(`mvp_voti?match_id=like.${PREFISSO}-*`, { method: "DELETE" });
|
||||
await rest(`pagelle_voti?match_id=like.${PREFISSO}-*`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
riepilogo("s-tiebreak-badge");
|
||||
}
|
||||
@@ -40,6 +40,27 @@ assert.equal(vincitoreCategoria(voti, "m1", "cuore"), null, "nessun voto, nessun
|
||||
const pari = [v("m3", "meme", "g1", "g2", "Bruno"), v("m3", "meme", "g2", "g5", "Anna")];
|
||||
assert.equal(vincitoreCategoria(pari, "m3", "meme"), null, "parità: nessun vincitore");
|
||||
|
||||
// Un solo voto totale: vince comunque, non serve concorrenza per avere un vantaggio netto.
|
||||
const votoSingolo = [v("m4", "cuore", "g1", "g9", "Zoe")];
|
||||
assert.equal(
|
||||
vincitoreCategoria(votoSingolo, "m4", "cuore")?.nome,
|
||||
"Zoe",
|
||||
"un voto solo basta se non c'è nessun altro candidato",
|
||||
);
|
||||
|
||||
// Tre candidati: i primi due pari in testa, il terzo staccato. Il pareggio conta comunque,
|
||||
// non basta che qualcun altro sia sotto per assegnare la categoria.
|
||||
const triplaPari = [
|
||||
v("m5", "spirito", "g1", "g8", "Uno"),
|
||||
v("m5", "spirito", "g2", "g7", "Due"),
|
||||
v("m5", "spirito", "g3", "g6", "Tre"),
|
||||
];
|
||||
assert.equal(
|
||||
vincitoreCategoria(triplaPari, "m5", "spirito"),
|
||||
null,
|
||||
"primo e secondo pari: nessun vincitore anche con un terzo staccato",
|
||||
);
|
||||
|
||||
// --- mioVotoSocial -----------------------------------------------------------
|
||||
assert.equal(mioVotoSocial(voti, "m1", "affidabile", "g1")?.votato_id, "g2");
|
||||
assert.equal(mioVotoSocial(voti, "m1", "meme", "g3"), null, "non ho votato questa categoria");
|
||||
@@ -51,6 +72,19 @@ assert.deepEqual(badgeSocialVinti(voti, "g9"), {}, "chi non vince non ha badge")
|
||||
assert.deepEqual(badgeSocialVinti(pari, "g2"), {}, "una parità non assegna badge");
|
||||
assert.deepEqual(badgeSocialVinti([], "g2"), {});
|
||||
|
||||
// Categorie e partite diverse non si mischiano: g2 vince "affidabile" in m1/m2 (già sopra) e
|
||||
// "fairplay" in m2, un'altra categoria nella stessa partita — i due conteggi restano separati.
|
||||
const conAltraCategoria: VotoSocial[] = [
|
||||
...voti,
|
||||
v("m2", "fairplay", "g3", "g2", "Bruno"),
|
||||
v("m2", "fairplay", "g4", "g2", "Bruno"),
|
||||
];
|
||||
assert.deepEqual(
|
||||
badgeSocialVinti(conAltraCategoria, "g2"),
|
||||
{ affidabile: 2, fairplay: 1 },
|
||||
"vittorie in categorie diverse, anche nella stessa partita, si contano separate",
|
||||
);
|
||||
|
||||
// --- invarianti sulle categorie ----------------------------------------------
|
||||
assert.equal(
|
||||
new Set(categorieSocial.map((c) => c.id)).size,
|
||||
|
||||
@@ -90,6 +90,27 @@ assert.equal(
|
||||
"sopra la soglia minima, valgono le normali soglie di grado",
|
||||
);
|
||||
|
||||
// Confini argento/oro, non solo bronzo: stesso arrotondamento per difetto.
|
||||
assert.equal(gradoRaggiunto(pagella, 7.4), "bronzo");
|
||||
assert.equal(gradoRaggiunto(pagella, 7.5), "argento");
|
||||
assert.equal(gradoRaggiunto(pagella, 8.4), "argento");
|
||||
assert.equal(gradoRaggiunto(pagella, 8.5), "oro");
|
||||
|
||||
// Progresso con soglia decimale: valore/prossimaSoglia, non arrotondato per eccesso.
|
||||
assert.equal(
|
||||
statoBadge(pagella, g({ mediaVoto: 7, votiPagella: 5 })).progresso,
|
||||
93,
|
||||
"7/7.5 = 93.3%, arrotondato a 93",
|
||||
);
|
||||
|
||||
// Sotto la soglia minima di voti il valore è forzato a 0: anche il progresso torna a 0%,
|
||||
// non alla percentuale che la media reale avrebbe suggerito.
|
||||
assert.equal(
|
||||
statoBadge(pagella, g({ mediaVoto: 10, votiPagella: 1 })).progresso,
|
||||
0,
|
||||
"valore azzerato dal gate: progresso azzerato anch'esso, non ingannevole",
|
||||
);
|
||||
|
||||
// --- palloni: soglie 3 / 6 / 10 -----------------------------------------------
|
||||
const palloniDef = badgeDefs.find((b) => b.id === "palloni")!;
|
||||
assert.equal(gradoRaggiunto(palloniDef, 2), null, "sotto la prima soglia nessun grado");
|
||||
@@ -147,12 +168,22 @@ const nessunSegreto = g({ mvp: 2, mediaVoto: 7.9 });
|
||||
assert.equal(badgeSegretiSbloccati(nessunSegreto).length, 0, "serve media 8, non 7.9");
|
||||
assert.equal(segretiNascosti(nessunSegreto), badgeSegreti.length);
|
||||
|
||||
const tiebreak = badgeSegretiSbloccati(g({ mvp: 2, mediaVoto: 8 }));
|
||||
const tiebreak = badgeSegretiSbloccati(g({ mvp: 2, mediaVoto: 8, votiPagella: 5 }));
|
||||
assert.deepEqual(
|
||||
tiebreak.map((b) => b.def.id),
|
||||
["s-tiebreak"],
|
||||
"sblocca solo il segreto il cui requisito è soddisfatto",
|
||||
);
|
||||
assert.equal(
|
||||
badgeSegretiSbloccati(g({ mvp: 2, mediaVoto: 8, votiPagella: 4 })).length,
|
||||
0,
|
||||
"come Pagellone: sotto la soglia minima di voti la media non conta, nemmeno qui",
|
||||
);
|
||||
assert.equal(
|
||||
badgeSegretiSbloccati(g({ mvp: 1, mediaVoto: 8, votiPagella: 5 })).length,
|
||||
0,
|
||||
"un solo MVP non basta",
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
badgeSegretiSbloccati(g({ infortuni: 3 })).map((b) => b.def.id),
|
||||
@@ -163,10 +194,12 @@ assert.deepEqual(
|
||||
badgeSegretiSbloccati(g({ ritardi: 5 })).map((b) => b.def.id),
|
||||
["s-ritardi"],
|
||||
);
|
||||
assert.equal(badgeSegretiSbloccati(g({ ritardi: 4 })).length, 0, "4 ritardi non bastano");
|
||||
assert.deepEqual(
|
||||
badgeSegretiSbloccati(g({ cacche: 3 })).map((b) => b.def.id),
|
||||
["s-cacche"],
|
||||
);
|
||||
assert.equal(badgeSegretiSbloccati(g({ cacche: 2 })).length, 0, "2 cacche non bastano");
|
||||
assert.deepEqual(
|
||||
badgeSegretiSbloccati(g({ serieConferme: 10, presenze: 15 })).map((b) => b.def.id),
|
||||
["s-mai-forfait"],
|
||||
@@ -176,6 +209,27 @@ assert.equal(
|
||||
0,
|
||||
"servono entrambe le condizioni",
|
||||
);
|
||||
// Confini isolati: ogni soglia testata da sola, con l'altra abbondantemente sopra.
|
||||
assert.equal(
|
||||
badgeSegretiSbloccati(g({ serieConferme: 9, presenze: 30 })).length,
|
||||
0,
|
||||
"serieConferme appena sotto soglia, presenze abbondanti: non basta",
|
||||
);
|
||||
assert.deepEqual(
|
||||
badgeSegretiSbloccati(g({ serieConferme: 10, presenze: 30 })).map((b) => b.def.id),
|
||||
["s-mai-forfait"],
|
||||
"serieConferme esattamente al confine, presenze abbondanti: sblocca",
|
||||
);
|
||||
assert.equal(
|
||||
badgeSegretiSbloccati(g({ serieConferme: 30, presenze: 14 })).length,
|
||||
0,
|
||||
"presenze appena sotto soglia, serieConferme abbondante: non basta",
|
||||
);
|
||||
assert.deepEqual(
|
||||
badgeSegretiSbloccati(g({ serieConferme: 30, presenze: 15 })).map((b) => b.def.id),
|
||||
["s-mai-forfait"],
|
||||
"presenze esattamente al confine, serieConferme abbondante: sblocca",
|
||||
);
|
||||
|
||||
// --- collezioneBadge ---------------------------------------------------------
|
||||
const vuota = collezioneBadge(g());
|
||||
|
||||
@@ -99,4 +99,14 @@ assert.equal(
|
||||
1,
|
||||
);
|
||||
|
||||
// Uno stesso giocatore infortunato in un evento e in ritardo in un altro: i due conteggi
|
||||
// restano indipendenti, nessuno "ruba" all'altro.
|
||||
const misto: MappaPresenze = {
|
||||
e1: { g4: "infortunato" },
|
||||
e2: { g4: "ritardo" },
|
||||
e3: { g4: "infortunato" },
|
||||
};
|
||||
assert.deepEqual(contaInfortuni(misto, eventi, OGGI), { g4: 2 });
|
||||
assert.deepEqual(contaRitardi(misto, eventi, OGGI), { g4: 1 });
|
||||
|
||||
console.log("infortuni: ok");
|
||||
|
||||
@@ -52,6 +52,20 @@ assert.deepEqual(
|
||||
"un solo votante basta se non c'è concorrenza",
|
||||
);
|
||||
|
||||
// Tre candidati: i primi due pari in testa, un terzo staccato. Deve restare senza MVP,
|
||||
// non basta che il terzo sia sotto: conta solo il confronto fra il primo e il secondo.
|
||||
const triplaPari = [
|
||||
v("m6", "g1", "g9", "Zeno"),
|
||||
v("m6", "g2", "g8", "Anna"),
|
||||
v("m6", "g3", "g7", "Bea"),
|
||||
];
|
||||
assert.deepEqual(vincitoriMvp(triplaPari), {}, "primo e secondo pari: nessun MVP anche a 3 vie");
|
||||
assert.deepEqual(
|
||||
mvpVintiPerGiocatore(triplaPari),
|
||||
{},
|
||||
"stessa parità: nessuna vittoria netta da contare",
|
||||
);
|
||||
|
||||
// --- mioVoto -----------------------------------------------------------------
|
||||
assert.equal(mioVoto(partita, "m1", "g1")?.votato_nome, "Bruno");
|
||||
assert.equal(mioVoto(partita, "m1", "g9"), null, "chi non ha votato non ha voto");
|
||||
|
||||
@@ -121,6 +121,18 @@ assert.deepEqual(
|
||||
"nessun turno assegnato: conteggio vuoto",
|
||||
);
|
||||
|
||||
// A differenza di `eventiPalloni()` (che scarta i compleanni), `conteggioTurni()` non filtra
|
||||
// per tipo: guarda solo `turni` ed `e.data < oggi`. Un turno registrato per errore su un
|
||||
// evento che il resto del modulo tratterebbe come "non richiede palloni" conterebbe comunque
|
||||
// per il badge. Comportamento attuale documentato (non l'UI non offre questa combinazione),
|
||||
// non una correzione: se cambia, questo test deve fallire e ricordarlo.
|
||||
const conCompleanno: Evento[] = [evento("cb1", "2026-09-01", "compleanno")];
|
||||
assert.deepEqual(
|
||||
conteggioTurni({ cb1: "g1" }, conCompleanno, OGGI_CONTEGGIO),
|
||||
{ g1: 1 },
|
||||
"conteggioTurni() non esclude i compleanni come fa eventiPalloni(): nessun filtro per tipo",
|
||||
);
|
||||
|
||||
// --- oggiISO -------------------------------------------------------------------
|
||||
assert.match(oggiISO(), /^\d{4}-\d{2}-\d{2}$/);
|
||||
// Stesso controllo di dataOggi() in scout-live.test.ts: oggiISO() ne è un alias, il fuso
|
||||
|
||||
Reference in New Issue
Block a user