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>
350 lines
13 KiB
TypeScript
350 lines
13 KiB
TypeScript
/**
|
||
* 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");
|
||
}
|
||
}
|