// 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, 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 }); }); }); 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([]); }); });