Files
InsanityLab-web/apps/platforms/tests/longevity/andamento.test.ts
T
Adriano 1eb69e0317 longevity: #14 unità B, grafici dei test per cliente e trainer
AndamentoTest.tsx (recharts, isola React come Progressione.tsx): un
LineChart per grafico del catalogo di andamento.ts, asse X temporale vero
(timestamp, scala 'time'), dx/sx come due linee distinte per tratteggio
(mai colore), tooltip con valore+unità ed extra (etichetta/unità dal
registro), righe di cambio test in gg/mm/aaaa.

progressione.astro: sezione «I tuoi test» (andamentoTest trainer:false),
visibile anche con meno di 2 sessioni — il grafico degli assi resta con
la sua condizione ≥2 invariata.

Nuova gestionale/cliente/[code]/andamento.astro (trainer:true, + grafico
degli assi se ≥2 punti), link «Andamento» in [code].astro accanto a
«Nuovo check-up» (.lg-btn, già ≥44px).

Modifica ammessa in andamento.ts: LineaAndamento.extra (etichetta/unità
dell'extraTestId dal registro) così il tooltip non indovina — test in
andamento.test.ts.

Refs #14
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ToarVnK21BEig85QQowuw
2026-09-28 06:57:58 +00:00

270 lines
13 KiB
TypeScript

// tests/longevity/andamento.test.ts — issue #14, unità A (dati).
// andamentoTest legge le misure GREZZE di `misure` (mai `score`), un punto per sessione e
// per test_id: serie con buchi, doppie linee (dx/sx), tooltip del 5RM, cambio di test
// segnalato per sezione, misure fuori range escluse finché non confermate, visibilità
// trainer/cliente, test fuori catalogo in «Altre misure».
import { describe, it, expect } from 'vitest';
import { createLongevityDb } from '../../src/lib/longevity/db';
import { seedRegistro } from '../../src/lib/longevity/registro';
import { apriSessione, registraMisure, verificaMisura } from '../../src/lib/longevity/misure';
import { andamentoTest } from '../../src/lib/longevity/andamento';
function dbPronto() {
const db = createLongevityDb(':memory:');
seedRegistro(db);
db.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0001','F')`).run();
return db;
}
function checkup(db: ReturnType<typeof dbPronto>, data: string) {
return apriSessione(db, { client_code: 'ISL-0001', data, tipo: 'checkup', eta_alla_data: 40 });
}
describe('andamentoTest: serie nel tempo, buchi e doppie linee', () => {
it('un grafico con buchi: solo le sessioni con misura hanno un punto', () => {
const db = dbPronto();
const s1 = checkup(db, '2026-01-10');
registraMisure(db, s1, 'manuale', [{ test_id: 'f_peso_kg', valore_num: 80 }]);
const s2 = checkup(db, '2026-03-10');
registraMisure(db, s2, 'manuale', [{ test_id: 'f_squat_carico_kg', valore_num: 100 }]); // niente peso
const s3 = checkup(db, '2026-05-10');
registraMisure(db, s3, 'manuale', [{ test_id: 'f_peso_kg', valore_num: 78 }]);
const sezioni = andamentoTest(db, 'ISL-0001', { trainer: true });
const preTest = sezioni.find((s) => s.titolo === 'Pre-test')!;
const peso = preTest.grafici.find((g) => g.titolo === 'Peso')!;
expect(peso.linee).toHaveLength(1);
expect(peso.linee[0]!.punti).toEqual([
{ data: '2026-01-10', sessioneId: s1, valore: 80 },
{ data: '2026-05-10', sessioneId: s3, valore: 78 },
]);
});
it('destro e sinistro sono due linee nello stesso grafico', () => {
const db = dbPronto();
const s1 = checkup(db, '2026-01-10');
registraMisure(db, s1, 'manuale', [
{ test_id: 'f_handgrip_dx_kg', valore_num: 32 },
{ test_id: 'f_handgrip_sx_kg', valore_num: 28 },
]);
const sezioni = andamentoTest(db, 'ISL-0001', { trainer: true });
const presa = sezioni.find((s) => s.titolo === 'Presa')!;
const handgrip = presa.grafici.find((g) => g.titolo === 'Handgrip')!;
expect(handgrip.linee.map((l) => l.testId).sort()).toEqual(['f_handgrip_dx_kg', 'f_handgrip_sx_kg']);
expect(handgrip.unita).toBe('kg');
const dx = handgrip.linee.find((l) => l.testId === 'f_handgrip_dx_kg')!;
expect(dx.punti[0]!.valore).toBe(32);
// etichetta letta dal registro (non ripetuta nel catalogo): contiene "destro".
expect(dx.etichetta.toLowerCase()).toContain('destro');
});
it('il tooltip del 5RM porta le ripetizioni della stessa sessione, mai come linea a sé', () => {
const db = dbPronto();
const s1 = checkup(db, '2026-01-10');
registraMisure(db, s1, 'manuale', [
{ test_id: 'f_row_carico_kg', valore_num: 60 },
{ test_id: 'f_row_rip', valore_num: 5 },
]);
const sezioni = andamentoTest(db, 'ISL-0001', { trainer: true });
const trazione = sezioni.find((s) => s.titolo === 'Trazione')!;
const rematore = trazione.grafici.find((g) => g.titolo === 'Rematore')!;
expect(rematore.linee).toHaveLength(1); // f_row_rip non è una linea a sé
expect(rematore.linee[0]!.testId).toBe('f_row_carico_kg');
expect(rematore.linee[0]!.punti[0]).toEqual({ data: '2026-01-10', sessioneId: s1, valore: 60, extra: 5 });
});
it('la linea con extraTestId porta etichetta e unità del valore in tooltip, lette dal registro (unità B non indovina)', () => {
const db = dbPronto();
const s1 = checkup(db, '2026-01-10');
registraMisure(db, s1, 'manuale', [
{ test_id: 'f_row_carico_kg', valore_num: 60 },
{ test_id: 'f_row_rip', valore_num: 5 },
{ test_id: 'f_walk2km_min', valore_num: 18 },
{ test_id: 'f_walk2km_fc_bpm', valore_num: 140 },
{ test_id: 'f_peso_kg', valore_num: 80 },
]);
const sezioni = andamentoTest(db, 'ISL-0001', { trainer: true });
const rematore = sezioni.find((s) => s.titolo === 'Trazione')!.grafici.find((g) => g.titolo === 'Rematore')!;
// f_row_rip in registro.ts non ha `unita`: deve arrivare null, mai undefined (silenzioso).
expect(rematore.linee[0]!.extra).toEqual({ etichetta: 'Trazione/rematore, ripetizioni (test 5RM)', unita: null });
const camminata = sezioni.find((s) => s.titolo === 'Aerobico')!.grafici.find((g) => g.titolo === 'Camminata 2 km')!;
expect(camminata.linee[0]!.extra).toEqual({ etichetta: 'Camminata 2 km (UKK), frequenza cardiaca finale', unita: 'bpm' });
// Una linea senza extraTestId (es. Peso) non ha proprietà `extra`.
const peso = sezioni.find((s) => s.titolo === 'Pre-test')!.grafici.find((g) => g.titolo === 'Peso')!;
expect(peso.linee[0]!.extra).toBeUndefined();
});
});
describe('andamentoTest: cambio di test dentro la stessa sezione (Trazione)', () => {
it('rematore -> trazioni -> rematore: due cambi, con la data giusta', () => {
const db = dbPronto();
const s1 = checkup(db, '2026-01-10');
registraMisure(db, s1, 'manuale', [{ test_id: 'f_row_carico_kg', valore_num: 60 }, { test_id: 'f_row_rip', valore_num: 5 }]);
const s2 = checkup(db, '2026-03-10');
registraMisure(db, s2, 'manuale', [{ test_id: 'f_row_carico_kg', valore_num: 65 }, { test_id: 'f_row_rip', valore_num: 5 }]);
const s3 = checkup(db, '2026-05-10');
registraMisure(db, s3, 'manuale', [{ test_id: 'f_trazioni_rip', valore_num: 8 }]);
const s4 = checkup(db, '2026-07-10');
registraMisure(db, s4, 'manuale', [{ test_id: 'f_row_carico_kg', valore_num: 70 }, { test_id: 'f_row_rip', valore_num: 6 }]);
const sezioni = andamentoTest(db, 'ISL-0001', { trainer: true });
const trazione = sezioni.find((s) => s.titolo === 'Trazione')!;
expect(trazione.cambi).toEqual([
{ data: '2026-05-10', da: 'Rematore', a: 'Trazioni' },
{ data: '2026-07-10', da: 'Trazioni', a: 'Rematore' },
]);
// entrambi i grafici restano, coi loro punti: un cambio non unisce le due serie.
// Rematore ha 3 punti (s1, s2, s4): un cambio segna il momento, non cancella la serie.
expect(trazione.grafici.find((g) => g.titolo === 'Rematore')!.linee[0]!.punti).toHaveLength(3);
expect(trazione.grafici.find((g) => g.titolo === 'Trazioni')!.linee[0]!.punti).toHaveLength(1);
});
it('un solo test in tutta la sezione: nessun cambio', () => {
const db = dbPronto();
const s1 = checkup(db, '2026-01-10');
registraMisure(db, s1, 'manuale', [{ test_id: 'f_trazioni_rip', valore_num: 8 }]);
const s2 = checkup(db, '2026-03-10');
registraMisure(db, s2, 'manuale', [{ test_id: 'f_trazioni_rip', valore_num: 9 }]);
const sezioni = andamentoTest(db, 'ISL-0001', { trainer: true });
const trazione = sezioni.find((s) => s.titolo === 'Trazione')!;
expect(trazione.cambi).toEqual([]);
});
});
describe('andamentoTest: i cambi valgono solo fra alternative vere, non nell\'intera sezione', () => {
it('Strumenti: massa grassa poi solo WHR non e\' un cambio (test non alternativi)', () => {
const db = dbPronto();
const s1 = checkup(db, '2026-01-10');
registraMisure(db, s1, 'manuale', [{ test_id: 'f_grasso_pct', valore_num: 20 }]);
const s2 = checkup(db, '2026-03-10');
registraMisure(db, s2, 'manuale', [{ test_id: 'f_whr', valore_num: 0.9 }]); // niente massa grassa
const sezioni = andamentoTest(db, 'ISL-0001', { trainer: true });
const strumenti = sezioni.find((s) => s.titolo === 'Strumenti')!;
expect(strumenti.grafici.map((g) => g.titolo).sort()).toEqual(['Massa grassa', 'Rapporto vita-fianchi (WHR)']);
expect(strumenti.cambi).toEqual([]);
});
it('Mobilità: back scratch in ogni sessione, sit and reach solo in una, non e\' un cambio', () => {
const db = dbPronto();
const s1 = checkup(db, '2026-01-10');
registraMisure(db, s1, 'manuale', [
{ test_id: 'f_back_scratch_dx_cm', valore_num: -5 },
{ test_id: 'f_back_scratch_sx_cm', valore_num: -6 },
]);
const s2 = checkup(db, '2026-03-10');
registraMisure(db, s2, 'manuale', [
{ test_id: 'f_back_scratch_dx_cm', valore_num: -4 },
{ test_id: 'f_back_scratch_sx_cm', valore_num: -5 },
{ test_id: 'f_sit_reach_cm', valore_num: 2 }, // solo in questa sessione
]);
const sezioni = andamentoTest(db, 'ISL-0001', { trainer: true });
const mobilita = sezioni.find((s) => s.titolo === 'Mobilità')!;
expect(mobilita.grafici.map((g) => g.titolo).sort()).toEqual(['Back scratch', 'Sit and reach']);
expect(mobilita.cambi).toEqual([]);
});
it('Presa: handgrip storico poi handgrip dx/sx e\' un vero cambio (alternativa dismessa)', () => {
const db = dbPronto();
const s1 = checkup(db, '2026-01-10');
registraMisure(db, s1, 'manuale', [{ test_id: 'f_handgrip_kg', valore_num: 30 }]);
const s2 = checkup(db, '2026-03-10');
registraMisure(db, s2, 'manuale', [
{ test_id: 'f_handgrip_dx_kg', valore_num: 32 },
{ test_id: 'f_handgrip_sx_kg', valore_num: 28 },
]);
const sezioni = andamentoTest(db, 'ISL-0001', { trainer: true });
const presa = sezioni.find((s) => s.titolo === 'Presa')!;
expect(presa.cambi).toEqual([{ data: '2026-03-10', da: 'Handgrip (storico)', a: 'Handgrip' }]);
});
});
describe('andamentoTest: solo misure valide', () => {
it('fuori range non confermata è esclusa; confermata entra', () => {
const db = dbPronto();
const s1 = checkup(db, '2026-01-10');
// f_milfit_watt: range 20-500 (registro.ts) — 999 è fuori range.
const { fuoriRange } = registraMisure(db, s1, 'manuale', [{ test_id: 'f_milfit_watt', valore_num: 999 }]);
expect(fuoriRange).toEqual(['f_milfit_watt']);
let sezioni = andamentoTest(db, 'ISL-0001', { trainer: true });
expect(sezioni.find((s) => s.titolo === 'Aerobico')).toBeUndefined();
const misuraRiga = db.prepare(
`SELECT id FROM misure WHERE sessione_id = ? AND test_id = 'f_milfit_watt'`
).get(s1) as { id: number };
const esito = verificaMisura(db, { misuraId: misuraRiga.id, esito: 'confermata', operatore: 'nicola' });
expect(esito.ok).toBe(true);
sezioni = andamentoTest(db, 'ISL-0001', { trainer: true });
const aerobico = sezioni.find((s) => s.titolo === 'Aerobico')!;
const milfit = aerobico.grafici.find((g) => g.titolo === 'MILFIT')!;
expect(milfit.linee[0]!.punti).toEqual([{ data: '2026-01-10', sessioneId: s1, valore: 999 }]);
});
});
describe('andamentoTest: visibilità trainer vs cliente', () => {
it('il cliente non vede i grafici soloTrainer, il trainer sì', () => {
const db = dbPronto();
const s1 = checkup(db, '2026-01-10');
registraMisure(db, s1, 'manuale', [
{ test_id: 'f_peso_kg', valore_num: 80 },
{ test_id: 'f_fc_riposo_bpm', valore_num: 60 },
]);
const trainer = andamentoTest(db, 'ISL-0001', { trainer: true });
const preTrainer = trainer.find((s) => s.titolo === 'Pre-test')!;
expect(preTrainer.grafici.map((g) => g.titolo).sort()).toEqual(['Frequenza cardiaca a riposo', 'Peso']);
const cliente = andamentoTest(db, 'ISL-0001', { trainer: false });
const preCliente = cliente.find((s) => s.titolo === 'Pre-test')!;
expect(preCliente.grafici.map((g) => g.titolo)).toEqual(['Peso']);
});
it('una sezione fatta solo di grafici soloTrainer sparisce del tutto per il cliente', () => {
const db = dbPronto();
const s1 = checkup(db, '2026-01-10');
registraMisure(db, s1, 'manuale', [{ test_id: 'f_fc_riposo_bpm', valore_num: 60 }]);
const cliente = andamentoTest(db, 'ISL-0001', { trainer: false });
expect(cliente.find((s) => s.titolo === 'Pre-test')).toBeUndefined();
const trainer = andamentoTest(db, 'ISL-0001', { trainer: true });
expect(trainer.find((s) => s.titolo === 'Pre-test')).toBeDefined();
});
});
describe('andamentoTest: test fuori catalogo', () => {
it('un test f_* misurato ma non nel catalogo finisce in «Altre misure», mai perso', () => {
const db = dbPronto();
const s1 = checkup(db, '2026-01-10');
registraMisure(db, s1, 'manuale', [{ test_id: 'f_altezza_cm', valore_num: 175 }]);
const sezioni = andamentoTest(db, 'ISL-0001', { trainer: true });
const altre = sezioni.find((s) => s.titolo === 'Altre misure')!;
expect(altre).toBeDefined();
expect(altre.grafici).toHaveLength(1);
expect(altre.grafici[0]!.linee[0]!.testId).toBe('f_altezza_cm');
expect(altre.grafici[0]!.unita).toBe('cm');
expect(altre.grafici[0]!.linee[0]!.punti[0]!.valore).toBe(175);
});
});
describe('andamentoTest: cliente senza misure', () => {
it('nessuna sessione, nessuna misura -> nessuna sezione', () => {
const db = dbPronto();
db.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0002','M')`).run();
const sezioni = andamentoTest(db, 'ISL-0002', { trainer: true });
expect(sezioni).toEqual([]);
});
});