diff --git a/test/integration/obiettivi.test.ts b/test/integration/obiettivi.test.ts index 2eb31b1..77dcdb9 100644 --- a/test/integration/obiettivi.test.ts +++ b/test/integration/obiettivi.test.ts @@ -1,7 +1,9 @@ /** - * Obiettivi di squadra con scadenza/mese dinamici ("presenze del mese", "evento di squadra al - * mese" e "tutti rispondono alle convocazioni") end-to-end contro il database locale: - * `bun test/integration/obiettivi.test.ts`. + * Obiettivi di squadra end-to-end contro il database locale: `bun test/integration/obiettivi.test.ts`. + * Copre gli obiettivi con scadenza/mese dinamici ("presenze del mese", "evento di squadra al + * mese", "tutti rispondono alle convocazioni") e quelli la cui logica dipende da dati scritti + * su altre tabelle ("250 presenze complessive" via `contaPresenzeGiocatore()`, "media pagelle + * da 7.5" via `pagelle_voti`). * * I test unitari (`test/unit/obiettivi.test.ts`) verificano `obiettiviSquadra()` come funzione * pura, con un `ContestoObiettivi` costruito a mano. Qui invece si scrivono righe vere su @@ -172,6 +174,103 @@ if (!locale) { } }); + await prova( + "o1 aggrega su più eventi reali dello stesso mese, incluse le partite", + async () => { + // Isolati dal resto del database (come per o2/o7/o12): l'evento "e1" di un test + // precedente resta nello stesso mese e andrebbe a sporcare l'aggregazione se non + // filtrassimo sui soli eventi di questo blocco. + const allenamentoId = `${PREFISSO}-o1b-allenamento`; + const partitaId = `${PREFISSO}-o1b-partita`; + for (const [id, tipo, data] of [ + [allenamentoId, "allenamento", `${MESE_TEST}-12`], + [partitaId, "partita", `${MESE_TEST}-19`], + ] as const) { + const inserito = await rest("eventi_app", { + method: "POST", + body: JSON.stringify({ id, tipo, titolo: `Test obiettivi o1 (${tipo})`, data }), + }); + if (!inserito.ok) throw new Error(`inserimento evento fallito: ${await inserito.text()}`); + } + + // Tutta la rosa presente all'allenamento, nessuno alla partita: 50% aggregato sui due. + const risposte = [ + ...giocatori.map((g) => ({ evento_id: allenamentoId, giocatore_id: g.id, stato: "presente" })), + ...giocatori.map((g) => ({ evento_id: partitaId, giocatore_id: g.id, stato: "assente" })), + ]; + const scritte = await rest("risposte_presenze", { + method: "POST", + body: JSON.stringify(risposte), + }); + if (!scritte.ok) throw new Error(`inserimento presenze fallito: ${await scritte.text()}`); + + const eventiReali = (await leggiEventi()).filter( + (e) => e.id === allenamentoId || e.id === partitaId, + ); + const presenzeReali = { + ...(await leggiPresenze(allenamentoId)), + ...(await leggiPresenze(partitaId)), + }; + + const o1 = obiettiviSquadra( + giocatori, + { eventi: eventiReali, presenze: presenzeReali, pagelle: [] }, + OGGI, + ).find((o) => o.id === "o1")!; + assert.equal( + o1.valore, + 50, + "le partite contano quanto gli allenamenti: 100% + 0% su due eventi reali = 50%", + ); + }, + ); + + await prova("o2 aggrega le risposte su più eventi reali, non solo su uno", async () => { + const eventoA = `${PREFISSO}-o2b-a`; + const eventoB = `${PREFISSO}-o2b-b`; + for (const [id, data] of [ + [eventoA, `${MESE_TEST}-02`], + [eventoB, `${MESE_TEST}-22`], + ] as const) { + const inserito = await rest("eventi_app", { + method: "POST", + body: JSON.stringify({ + id, + tipo: "allenamento", + titolo: "Test obiettivi o2 aggregato", + data, + }), + }); + if (!inserito.ok) throw new Error(`inserimento evento fallito: ${await inserito.text()}`); + } + + // Tutti rispondono al primo evento, nessuno al secondo: 50% aggregato sui due. + const risposteA = giocatori.map((g) => ({ + evento_id: eventoA, + giocatore_id: g.id, + stato: "presente", + })); + const scritte = await rest("risposte_presenze", { + method: "POST", + body: JSON.stringify(risposteA), + }); + if (!scritte.ok) throw new Error(`inserimento presenze fallito: ${await scritte.text()}`); + + const eventiReali = (await leggiEventi()).filter((e) => e.id === eventoA || e.id === eventoB); + const presenzeReali = await leggiPresenze(eventoA); + + const o2 = obiettiviSquadra( + giocatori, + { eventi: eventiReali, presenze: presenzeReali, pagelle: [] }, + OGGI, + ).find((o) => o.id === "o2")!; + assert.equal( + o2.valore, + 50, + "risposte piene su un evento, zero sull'altro = 50% aggregato sui due", + ); + }); + await prova( "o6 legge dal database l'evento sociale del mese, azzerandosi come o1", async () => { @@ -339,9 +438,40 @@ if (!locale) { ); }, ); + + await prova("o12 media pagelle vere lette da pagelle_voti", async () => { + const matchId = `${PREFISSO}-o12-m1`; + const [g1, g2, g3] = giocatori; + // 7 + 7 + 9 = 23 -> media 7.666... arrotondata a 7.7. Vincoli reali della tabella: + // niente autovoto (pagelle_no_autovoto), voto 1-10 (pagelle_voto_range). + const voti = [ + { match_id: matchId, votante_id: g1!.id, votato_id: g2!.id, voto: 7 }, + { match_id: matchId, votante_id: g2!.id, votato_id: g1!.id, voto: 7 }, + { match_id: matchId, votante_id: g3!.id, votato_id: g1!.id, voto: 9 }, + ]; + const inseriti = await rest("pagelle_voti", { method: "POST", body: JSON.stringify(voti) }); + if (!inseriti.ok) throw new Error(`inserimento pagelle fallito: ${await inseriti.text()}`); + + const lette = await rest( + `pagelle_voti?match_id=eq.${matchId}&select=match_id,votante_id,votato_id,voto`, + ); + const pagelleReali = (await lette.json()) as Array<{ + match_id: string; + votante_id: string; + votato_id: string; + voto: number; + }>; + assert.equal(pagelleReali.length, 3, "i tre voti sono stati scritti e riletti dal database"); + + const o12 = obiettiviSquadra(giocatori, { eventi: [], presenze: {}, pagelle: pagelleReali }).find( + (o) => o.id === "o12", + )!; + assert.equal(o12.valore, 7.7, "media dei voti reali, arrotondata a una cifra decimale"); + }); } finally { await rest(`risposte_presenze?evento_id=like.${PREFISSO}*`, { method: "DELETE" }); await rest(`eventi_app?id=like.${PREFISSO}*`, { method: "DELETE" }); + await rest(`pagelle_voti?match_id=like.${PREFISSO}*`, { method: "DELETE" }); riepilogo("obiettivi"); } } diff --git a/test/unit/obiettivi.test.ts b/test/unit/obiettivi.test.ts index fd7f85b..d1db347 100644 --- a/test/unit/obiettivi.test.ts +++ b/test/unit/obiettivi.test.ts @@ -136,6 +136,83 @@ assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni, OGGI_AGOSTO), "o2 assert.equal(o2Settembre.scadenza, "2026-09-30", "scadenza o2 cambia con il mese iniettato"); } +// --- o1/o2: rosa vuota, filtro sui tipi di evento, aggregazione su più eventi --- +{ + const evetoAgostoSingolo: ContestoObiettivi = { + eventi: [evento("rv1", "2026-08-10", "allenamento")], + presenze: { rv1: { g1: "presente" } }, + pagelle: [], + }; + assert.equal( + trova(obiettiviSquadra([], evetoAgostoSingolo, OGGI_AGOSTO), "o1").valore, + 0, + "rosa vuota: 0%, non divide per zero (o1)", + ); + assert.equal( + trova(obiettiviSquadra([], evetoAgostoSingolo, OGGI_AGOSTO), "o2").valore, + 0, + "rosa vuota: 0%, non divide per zero (o2)", + ); + + // o1 conta solo partita+allenamento: se "evento"/"compleanno" trapelassero nel calcolo, + // il risultato scenderebbe dal 100% atteso (nessuno "presente" su quei due). + const filtriTipo: ContestoObiettivi = { + eventi: [ + evento("ft-partita", "2026-08-05", "partita"), + evento("ft-allenamento", "2026-08-06", "allenamento"), + evento("ft-evento", "2026-08-07", "evento"), + evento("ft-compleanno", "2026-08-08", "compleanno"), + ], + presenze: { + "ft-partita": Object.fromEntries(giocatori.map((g) => [g.id, "presente" as const])), + "ft-allenamento": Object.fromEntries(giocatori.map((g) => [g.id, "presente" as const])), + "ft-evento": Object.fromEntries(giocatori.map((g) => [g.id, "assente" as const])), + "ft-compleanno": {}, + }, + pagelle: [], + }; + assert.equal( + trova(obiettiviSquadra(giocatori, filtriTipo, OGGI_AGOSTO), "o1").valore, + 100, + "o1 conta le partite come gli allenamenti, ignora eventi sociali e compleanni", + ); + + // Aggregazione su più eventi dello stesso mese: 100% su uno, 0% sull'altro = 50% aggregato. + const dueEventi: ContestoObiettivi = { + eventi: [ + evento("de1", "2026-08-03", "allenamento"), + evento("de2", "2026-08-17", "allenamento"), + ], + presenze: { + de1: Object.fromEntries(giocatori.map((g) => [g.id, "presente" as const])), + de2: Object.fromEntries(giocatori.map((g) => [g.id, "assente" as const])), + }, + pagelle: [], + }; + assert.equal( + trova(obiettiviSquadra(giocatori, dueEventi, OGGI_AGOSTO), "o1").valore, + 50, + "o1 aggrega su più eventi dello stesso mese, non solo sull'ultimo", + ); + + const dueEventiRisposte: ContestoObiettivi = { + eventi: [ + evento("dr1", "2026-08-03", "allenamento"), + evento("dr2", "2026-08-17", "partita"), + ], + presenze: { + dr1: Object.fromEntries(giocatori.map((g) => [g.id, "presente" as const])), + dr2: {}, + }, + pagelle: [], + }; + assert.equal( + trova(obiettiviSquadra(giocatori, dueEventiRisposte, OGGI_AGOSTO), "o2").valore, + 50, + "o2 aggrega le risposte su più eventi, non solo sull'ultimo", + ); +} + // --- o6: evento di squadra al mese, si azzera come o1 ------------------------ { const OGGI_SETTEMBRE = new Date("2026-09-05T10:00:00Z"); @@ -193,6 +270,31 @@ const conPagelle = obiettiviSquadra(giocatori, { ...contestoVuoto, pagelle }); assert.equal(trova(conPagelle, "o12").valore, 7.5); assert.equal(trova(conPagelle, "o13").valore, 2, "conta i voti compilati"); +// La media arrotonda a una cifra decimale, non tronca: 23/3 = 7.666... -> 7.7. +const pagelleDaArrotondare: VotoPagella[] = [ + { match_id: "m2", votante_id: "g1", votato_id: "g2", voto: 7 }, + { match_id: "m2", votante_id: "g2", votato_id: "g1", voto: 7 }, + { match_id: "m2", votante_id: "g3", votato_id: "g1", voto: 9 }, +]; +assert.equal( + trova(obiettiviSquadra(giocatori, { ...contestoVuoto, pagelle: pagelleDaArrotondare }), "o12") + .valore, + 7.7, + "media arrotondata a una cifra decimale (23/3 = 7.666... -> 7.7)", +); + +// La media aggrega i voti di più partite insieme, non solo dell'ultima. +const pagellePiuPartite: VotoPagella[] = [ + { match_id: "m3", votante_id: "g1", votato_id: "g2", voto: 5 }, + { match_id: "m4", votante_id: "g1", votato_id: "g2", voto: 9 }, +]; +assert.equal( + trova(obiettiviSquadra(giocatori, { ...contestoVuoto, pagelle: pagellePiuPartite }), "o12") + .valore, + 7, + "la media aggrega i voti di più partite, non guarda solo una match_id", +); + // --- progressoObiettivo ------------------------------------------------------ const o = (valore: number, target: number): ObiettivoSquadra => ({ id: "x",