Files
CRAPP/test/integration/scritture.test.ts
T
davideandClaude Sonnet 5.5 51b7a10c99 Scout live: valutazioni − / + per fondamentale e per set
Per ogni convocato un − o un + su battuta, ricezione, attacco e muro, separato per set e
fuori dal punteggio (DD-053, migration m39: colonna scout_partite.valutazioni). Lo stato in
corso le salva in scout_live.stato; il CSV admin ha la tabella per set.

Nella pagina dello scout tolti il blocco avversario/campo, i tasti delle azioni e il
tabellone del punteggio; il titolo della sezione mostra cognome e numero del giocatore.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 08:16:39 +01:00

350 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Le scritture dell'app contro il database locale: `bun test/integration/scritture.test.ts`.
*
* Ogni salvataggio di CrAPP è un `upsert` con un `onConflict` scritto a mano nei hook di
* `src/lib/`. Se quella chiave non corrisponde al vincolo UNIQUE della tabella non arriva
* nessun errore: il database sovrascrive la riga sbagliata, e il bug si vede solo settimane
* dopo in una media che non torna. Qui si ripetono le stesse chiamate dei hook e si conta
* cosa resta nella tabella.
*
* Gira solo sullo stack locale (`npx supabase start`) e cancella le proprie righe alla fine:
* usa id con il prefisso `test-scritture`, che nessun dato vero può avere.
*/
import assert from "node:assert/strict";
import { statoLocale } from "../helpers/locale";
import { prova, riepilogo, salta } from "../helpers/prova";
const locale = statoLocale();
if (!locale) {
salta("scritture sul database", "stack locale non attivo (npx supabase start)");
riepilogo("scritture");
} else {
const { url: URL_BASE, servizio: SERVIZIO } = locale;
console.log(`scritture su ${URL_BASE}`);
const PREFISSO = "test-scritture";
const TABELLE = [
"pagelle_voti",
"mvp_voti",
"cacche_partita",
"badge_social_voti",
"turni_palloni",
"risposte_presenze",
"scout_live",
"scout_partite",
] as const;
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 ?? {}),
},
});
/**
* Lo stesso `upsert` che fa supabase-js: `onConflict` diventa `on_conflict` nella query
* e `resolution=merge-duplicates` nell'header.
*/
async function upsert(tabella: string, onConflict: string, riga: Record<string, unknown>) {
const res = await rest(`${tabella}?on_conflict=${onConflict}`, {
method: "POST",
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
body: JSON.stringify(riga),
});
if (!res.ok) throw new Error(`upsert su ${tabella}: ${res.status} ${await res.text()}`);
return (await res.json()) as Array<Record<string, unknown>>;
}
const leggi = async (tabella: string, filtro: string) =>
(await (await rest(`${tabella}?${filtro}`)).json()) as Array<Record<string, unknown>>;
try {
// --- pagelle: la chiave più facile da sbagliare -------------------------------
// Un votante dà un voto a ogni compagno nella stessa partita: se il conflitto
// fosse su (match, votante) ogni voto cancellerebbe il precedente.
await prova("le pagelle tengono un voto per ogni votato, non uno per votante", async () => {
const match = `${PREFISSO}-m1`;
const chiave = "match_id,votante_id,votato_id";
await upsert("pagelle_voti", chiave, {
match_id: match,
votante_id: "g1",
votato_id: "g2",
voto: 6,
});
await upsert("pagelle_voti", chiave, {
match_id: match,
votante_id: "g1",
votato_id: "g3",
voto: 8,
});
const righe = await leggi("pagelle_voti", `match_id=eq.${match}&select=votato_id,voto`);
assert.equal(righe.length, 2, "due votati, due righe");
await upsert("pagelle_voti", chiave, {
match_id: match,
votante_id: "g1",
votato_id: "g2",
voto: 9,
});
const dopo = await leggi("pagelle_voti", `match_id=eq.${match}&votato_id=eq.g2&select=voto`);
assert.equal(dopo.length, 1, "cambiare voto non crea una riga nuova");
assert.equal(dopo[0]?.["voto"], 9, "il voto è quello aggiornato");
});
await prova("le pagelle rifiutano l'autovoto e i voti fuori scala", async () => {
const match = `${PREFISSO}-m2`;
const chiave = "match_id,votante_id,votato_id";
await assert.rejects(
() =>
upsert("pagelle_voti", chiave, {
match_id: match,
votante_id: "g1",
votato_id: "g1",
voto: 10,
}),
"nessuno si vota da solo (pagelle_no_autovoto)",
);
await assert.rejects(
() =>
upsert("pagelle_voti", chiave, {
match_id: match,
votante_id: "g1",
votato_id: "g2",
voto: 11,
}),
"il voto sta fra 1 e 10 (pagelle_voto_range)",
);
});
// --- MVP: qui la regola è l'opposta -------------------------------------------
await prova("l'MVP e i badge social rifiutano l'autovoto", async () => {
// M12: `pagelle_no_autovoto` esisteva dalla v1.0, queste due tabelle no. Il vincolo sta
// a database perché l'interfaccia non è l'unica strada per scrivere una riga.
await assert.rejects(
() =>
upsert("mvp_voti", "match_id,votante_id", {
match_id: `${PREFISSO}-m3`,
votante_id: "g1",
votato_id: "g1",
votato_nome: "Uno",
}),
"nessuno si elegge MVP da solo (mvp_no_autovoto)",
);
await assert.rejects(
() =>
upsert("badge_social_voti", "match_id,categoria,votante_id", {
match_id: `${PREFISSO}-m3`,
categoria: "cuore",
votante_id: "g1",
votato_id: "g1",
votato_nome: "Uno",
}),
"né si assegna un badge social (badge_social_no_autovoto)",
);
});
await prova("l'MVP tiene un solo voto per votante e partita", async () => {
const match = `${PREFISSO}-m3`;
const chiave = "match_id,votante_id";
await upsert("mvp_voti", chiave, {
match_id: match,
votante_id: "g1",
votato_id: "g2",
votato_nome: "Due",
});
await upsert("mvp_voti", chiave, {
match_id: match,
votante_id: "g1",
votato_id: "g3",
votato_nome: "Tre",
});
const righe = await leggi("mvp_voti", `match_id=eq.${match}&select=votato_id`);
assert.equal(righe.length, 1, "cambiare idea sostituisce il voto, non lo aggiunge");
assert.equal(righe[0]?.["votato_id"], "g3", "vale l'ultimo votato");
});
// --- badge social: una preferenza per categoria --------------------------------
await prova("i badge social tengono un voto per categoria", async () => {
const match = `${PREFISSO}-m4`;
const chiave = "match_id,categoria,votante_id";
await upsert("badge_social_voti", chiave, {
match_id: match,
categoria: "sorriso",
votante_id: "g1",
votato_id: "g2",
votato_nome: "Due",
});
await upsert("badge_social_voti", chiave, {
match_id: match,
categoria: "urlo",
votante_id: "g1",
votato_id: "g2",
votato_nome: "Due",
});
const righe = await leggi("badge_social_voti", `match_id=eq.${match}&select=categoria`);
assert.equal(righe.length, 2, "categorie diverse, righe diverse");
await upsert("badge_social_voti", chiave, {
match_id: match,
categoria: "urlo",
votante_id: "g1",
votato_id: "g4",
votato_nome: "Quattro",
});
const urlo = await leggi(
"badge_social_voti",
`match_id=eq.${match}&categoria=eq.urlo&select=votato_id`,
);
assert.equal(urlo.length, 1, "nella stessa categoria si sostituisce");
assert.equal(urlo[0]?.["votato_id"], "g4");
});
// --- cacche: una dichiarazione per giocatore e partita --------------------------
await prova("le cacche tengono una quantità per giocatore e partita", async () => {
const evento = `${PREFISSO}-e1`;
const chiave = "evento_id,giocatore_id";
await upsert("cacche_partita", chiave, {
evento_id: evento,
giocatore_id: "g1",
quantita: 2,
});
await upsert("cacche_partita", chiave, {
evento_id: evento,
giocatore_id: "g1",
quantita: 3,
});
await upsert("cacche_partita", chiave, {
evento_id: evento,
giocatore_id: "g2",
quantita: 1,
});
const righe = await leggi(
"cacche_partita",
`evento_id=eq.${evento}&select=giocatore_id,quantita&order=giocatore_id`,
);
assert.equal(righe.length, 2, "un giocatore una riga");
assert.equal(righe[0]?.["quantita"], 3, "la seconda dichiarazione sostituisce la prima");
});
// --- turni palloni: un solo incaricato per evento ------------------------------
await prova("il turno palloni resta uno per evento", async () => {
const evento = `${PREFISSO}-e2`;
await upsert("turni_palloni", "evento_id", {
evento_id: evento,
giocatore_id: "g1",
aggiornato_da: "g1",
});
await upsert("turni_palloni", "evento_id", {
evento_id: evento,
giocatore_id: "g5",
aggiornato_da: "g9",
});
const righe = await leggi(
"turni_palloni",
`evento_id=eq.${evento}&select=giocatore_id,aggiornato_da`,
);
assert.equal(righe.length, 1, "riassegnare non aggiunge un secondo incaricato");
assert.equal(righe[0]?.["giocatore_id"], "g5", "vale l'ultima assegnazione");
assert.equal(righe[0]?.["aggiornato_da"], "g9", "e si sa chi l'ha fatta");
});
// --- presenze: la risposta si cambia, il cronometro no --------------------------
// Due colonne che sembrano la stessa cosa e non lo sono: `risposto_il` è la PRIMA
// risposta, `aggiornato_il` è l'ultima modifica; oggi nessuna statistica le usa (la serie
// «Conferme 24h» è stata rimossa). Il trigger `risposte_presenze_risposto_il_immutabile` (M9) tiene ferma
// la prima: senza, chi risponde subito e ci ripensa una settimana dopo risulterebbe lento.
await prova(
"la risposta di presenza si aggiorna senza far ripartire il cronometro",
async () => {
const evento = `${PREFISSO}-e3`;
const chiave = "evento_id,giocatore_id";
const prima = await upsert("risposte_presenze", chiave, {
evento_id: evento,
giocatore_id: "g1",
stato: "presente",
aggiornato_il: new Date("2026-01-01T18:00:00Z").toISOString(),
});
await upsert("risposte_presenze", chiave, {
evento_id: evento,
giocatore_id: "g1",
stato: "assente",
aggiornato_il: new Date("2026-01-01T19:00:00Z").toISOString(),
// Il ripensamento prova anche a riscrivere l'istante della prima risposta: è
// esattamente la mossa che il trigger deve annullare.
risposto_il: new Date("2026-01-08T19:00:00Z").toISOString(),
});
const righe = await leggi(
"risposte_presenze",
`evento_id=eq.${evento}&select=stato,aggiornato_il,risposto_il`,
);
assert.equal(righe.length, 1, "una risposta per giocatore");
assert.equal(righe[0]?.["stato"], "assente", "vale l'ultima risposta");
assert.notEqual(
righe[0]?.["aggiornato_il"],
prima[0]?.["aggiornato_il"],
"l'ultima modifica si muove",
);
assert.equal(
righe[0]?.["risposto_il"],
prima[0]?.["risposto_il"],
"la prima risposta resta quella: il trigger di M9 la congela",
);
},
);
// --- scout live: lo stato viene sostituito, non fuso ----------------------------
await prova("lo scout live sostituisce lo stato invece di fonderlo", async () => {
const evento = `${PREFISSO}-e4`;
await upsert("scout_live", "evento_id", {
evento_id: evento,
stato: { set: 1, punti: 10 },
});
await upsert("scout_live", "evento_id", { evento_id: evento, stato: { set: 2 } });
const righe = await leggi("scout_live", `evento_id=eq.${evento}&select=stato`);
assert.equal(righe.length, 1);
assert.deepEqual(
righe[0]?.["stato"],
{ set: 2 },
"il jsonb viene rimpiazzato: i campi del set precedente non restano appesi",
);
});
// --- scout: le valutazioni − / + stanno accanto alle azioni (m39) ----------------
await prova(
"la partita scoutata conserva le valutazioni; senza, l'elenco è vuoto",
async () => {
const evento = `${PREFISSO}-e5`;
const base = {
evento_id: evento,
data: "2026-11-02",
avversario: "X",
set_nostri: 3,
set_loro: 0,
};
await upsert("scout_partite", "id", { ...base, id: `${PREFISSO}-s1` });
const valutazioni = [
{ id: "v1", giocatoreId: "g1", fondamentale: "battuta", segno: "+", set: 1, ts: 0 },
];
await upsert("scout_partite", "id", { ...base, id: `${PREFISSO}-s2`, valutazioni });
const righe = await leggi(
"scout_partite",
`evento_id=eq.${evento}&select=id,valutazioni&order=id`,
);
assert.deepEqual(righe[0]?.["valutazioni"], [], "le partite senza valutazioni hanno []");
assert.deepEqual(righe[1]?.["valutazioni"], valutazioni);
},
);
} finally {
for (const tabella of TABELLE) {
const colonna = tabella.endsWith("_voti") ? "match_id" : "evento_id";
await rest(`${tabella}?${colonna}=like.${PREFISSO}*`, { method: "DELETE" });
}
riepilogo("scritture");
}
}