1eb69e0317
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
270 lines
13 KiB
TypeScript
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([]);
|
|
});
|
|
});
|