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
203 lines
10 KiB
TypeScript
203 lines
10 KiB
TypeScript
// tests/longevity/pagine.test.ts
|
|
import { describe, it, expect } from 'vitest';
|
|
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
|
|
const leggi = (p: string) => readFileSync(join(process.cwd(), p), 'utf8');
|
|
|
|
describe('il guscio della piattaforma', () => {
|
|
it('il layout esiste e non monta header e footer pubblici', () => {
|
|
const l = leggi('src/layouts/Longevity.astro');
|
|
expect(l).not.toMatch(/import\s+Header/);
|
|
expect(l).not.toMatch(/import\s+Footer/);
|
|
expect(l).toMatch(/noindex/); // area riservata: mai indicizzata
|
|
});
|
|
|
|
it('nessuna pagina Longevity e senza uscita (#10): il referto porta sempre a Piattaforme, e a Gestionale per lo staff', () => {
|
|
const l = leggi('src/layouts/Longevity.astro');
|
|
expect(l).toMatch(/href=["']\/piattaforme["']/);
|
|
expect(l).toMatch(/href=["']\/longevity\/gestionale["']/);
|
|
// "Il mio referto" nel gestionale va mostrato solo a chi ha davvero un fascicolo.
|
|
expect(l).toMatch(/haFascicolo &&/);
|
|
});
|
|
|
|
it('io.astro offre un uscita al gestionale per chi non ha un fascicolo (staff)', () => {
|
|
const src = leggi('src/pages/longevity/io.astro');
|
|
expect(src).toMatch(/href=["']\/longevity\/gestionale["']/);
|
|
});
|
|
|
|
it('il foglio di stile e globale e prefissato, non scoped', () => {
|
|
expect(existsSync(join(process.cwd(), 'src/styles/longevity.css'))).toBe(true);
|
|
const css = leggi('src/styles/longevity.css');
|
|
const classi = [...css.matchAll(/^\.([a-z-]+)/gm)].map((m) => m[1]);
|
|
expect(classi.length).toBeGreaterThan(3);
|
|
// "lg" da solo o "lg-qualcosa": il solo startsWith('lg') lascerebbe passare anche "lgfoo".
|
|
expect(classi.every((c) => c === 'lg' || c.startsWith('lg-'))).toBe(true);
|
|
});
|
|
|
|
it('usa i token del sito invece di inventare colori', () => {
|
|
const css = leggi('src/styles/longevity.css');
|
|
expect(css).toMatch(/var\(--c-/);
|
|
});
|
|
|
|
it('nessuna variabile porta un nome da semaforo (verde/buono/ok)', () => {
|
|
const css = leggi('src/styles/longevity.css').toLowerCase();
|
|
// il mattone del fuori range e ammesso; un verde "tutto bene" no
|
|
expect(css).not.toMatch(/--lg-ok|--lg-buono|--lg-verde/);
|
|
});
|
|
|
|
it('in tutto il foglio compaiono solo i colori ammessi, salvo le eccezioni dichiarate', () => {
|
|
// Il test sopra cerca nomi vietati: chi scrivesse un verde in esadecimale, o lo
|
|
// chiamasse con un nome innocuo (var(--lg-buona-salute)), passerebbe indisturbato.
|
|
// Qui si fa il contrario: si elenca ciò che PUÒ comparire, e qualunque altro colore —
|
|
// letterale (esadecimale, rgb(), nome CSS) o var() con un altro nome — è una
|
|
// regressione.
|
|
//
|
|
// PRIMA questo test filtrava le regole sul selettore ("solo quelle con 'lg-score' nel
|
|
// nome"): un elenco di QUALI regole guardare invecchia appena nasce una classe nuova —
|
|
// è successo con .lg-radar__voce-nome, .lg-radar__intro, .lg-macro__nota (Task 3): non
|
|
// contengono la stringa "lg-score", quindi non venivano MAI controllate, in silenzio.
|
|
// Il foglio è interamente prefissato `lg-` per costruzione (verificato dal test sopra):
|
|
// non serve scegliere quali regole guardare, si guardano TUTTE, e le eccezioni
|
|
// legittime si dichiarano una per una — selettore, valore esatto, motivo — così un
|
|
// colore fuori lista entra solo con una decisione visibile, mai con un buco nel filtro.
|
|
const css = leggi('src/styles/longevity.css');
|
|
const AMMESSI = new Set([
|
|
'--c-accent', '--c-accent-dark', // l'accento del sito: l'unica leva per l'intensità
|
|
'--c-heading', '--c-text', '--c-text-light', // testo neutro, uguale ovunque nel sito
|
|
'--c-bg', '--c-bg-alt', // sfondi neutri
|
|
'--lg-line', '--lg-surface', // struttura locale, neutra
|
|
'--lg-tratteggio', // il perimetro di ciò che non sappiamo
|
|
'--lg-fuori-range', // SOLO una misura sospetta, mai un punteggio
|
|
]);
|
|
// Eccezioni dichiarate: selettore esatto + valore esatto + perché non è un giudizio sul
|
|
// punteggio. Ogni riga qui è una decisione presa a vista, non un buco silenzioso.
|
|
const ECCEZIONI: { selettore: string; valore: string; motivo: string }[] = [
|
|
{
|
|
selettore: '.lg-form__nav button:last-child:not(:only-child)',
|
|
valore: '#fff',
|
|
motivo: "testo bianco per contrasto sul bottone pieno d'accento — non un colore di punteggio",
|
|
},
|
|
];
|
|
const NON_COLORE = /^(transparent|inherit|initial|unset|currentcolor|none)$/i;
|
|
const PROP_COLORE = /^(color|background|background-color|border(-\w+)?-color|outline-color|fill|stroke)$/i;
|
|
|
|
const violazioni: string[] = [];
|
|
for (const regola of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
const selettore = regola[1].trim();
|
|
for (const dich of regola[2].split(';')) {
|
|
const due = dich.indexOf(':');
|
|
if (due < 0) continue;
|
|
const prop = dich.slice(0, due).trim().toLowerCase();
|
|
const val = dich.slice(due + 1).trim();
|
|
if (!PROP_COLORE.test(prop) || val === '' || NON_COLORE.test(val)) continue;
|
|
const varMatch = val.match(/^var\(\s*(--[a-z0-9-]+)\s*(?:,.*)?\)$/i);
|
|
if (varMatch && AMMESSI.has(varMatch[1])) continue;
|
|
if (ECCEZIONI.some((e) => e.selettore === selettore && e.valore === val)) continue;
|
|
violazioni.push(`${selettore} { ${prop}: ${val} }`);
|
|
}
|
|
}
|
|
expect(violazioni).toEqual([]);
|
|
});
|
|
|
|
it('la rotta del cliente e sotto /longevity, e rimanda davvero al referto', () => {
|
|
const p = join(process.cwd(), 'src/pages/longevity/index.astro');
|
|
expect(existsSync(p)).toBe(true);
|
|
const src = readFileSync(p, 'utf8');
|
|
expect(src).toMatch(/Astro\.redirect\(\s*['"`]\/longevity\/io['"`]/);
|
|
});
|
|
|
|
it('la pagina dell andamento non interroga il database e usa codicePerUtenteLoggato', () => {
|
|
// Rete secondaria alla stessa prova generica di referto.test.ts ("nessuna pagina
|
|
// interroga il database da sola", che scandisce TUTTE le .astro sotto longevity/):
|
|
// qui si controlla in piu' che il codice cliente arrivi dalla via autorizzata, non da
|
|
// un'altra query o da un parametro della richiesta.
|
|
const p = join(process.cwd(), 'src/pages/longevity/progressione.astro');
|
|
expect(existsSync(p)).toBe(true);
|
|
const src = readFileSync(p, 'utf8');
|
|
expect(src).not.toMatch(/\.prepare\(/);
|
|
expect(src).toMatch(/codicePerUtenteLoggato/);
|
|
// Stessa rete secondaria di anagrafica.test.ts: nessuna giunzione identity/longevity
|
|
// in un file fuori dai due moduli autorizzati.
|
|
expect(src).not.toContain('Identity');
|
|
});
|
|
|
|
it('progressione.astro usa andamento.ts con trainer:false (#14): il cliente non deve vedere i grafici riservati', () => {
|
|
const p = join(process.cwd(), 'src/pages/longevity/progressione.astro');
|
|
const src = readFileSync(p, 'utf8');
|
|
expect(src).toMatch(/from ['"`].*lib\/longevity\/andamento['"`]/);
|
|
expect(src).toMatch(/andamentoTest\([^)]*trainer:\s*false/);
|
|
});
|
|
});
|
|
|
|
describe('il gestionale del trainer (Unità 2b)', () => {
|
|
const PAGINE_GESTIONALE = [
|
|
'src/pages/longevity/gestionale/index.astro',
|
|
'src/pages/longevity/gestionale/nuovo.astro',
|
|
'src/pages/longevity/gestionale/cliente/[code].astro',
|
|
'src/pages/longevity/gestionale/cliente/[code]/checkup.astro',
|
|
'src/pages/longevity/gestionale/cliente/[code]/consulenza.astro', // #15
|
|
'src/pages/longevity/gestionale/cliente/[code]/andamento.astro', // #14
|
|
];
|
|
|
|
it('ogni pagina del gestionale esiste e usa la variante "gestionale" del layout', () => {
|
|
for (const p of PAGINE_GESTIONALE) {
|
|
expect(existsSync(join(process.cwd(), p))).toBe(true);
|
|
expect(leggi(p)).toMatch(/variante=["']gestionale["']/);
|
|
}
|
|
});
|
|
|
|
it('nessuna pagina del gestionale interroga il database direttamente', () => {
|
|
// Il gestionale legge sempre attraverso lib/longevity (anagrafica, gestionale, vista):
|
|
// una .prepare( dentro una .astro sarebbe una query scritta a mano nella pagina.
|
|
for (const p of PAGINE_GESTIONALE) {
|
|
expect(leggi(p)).not.toMatch(/\.prepare\(/);
|
|
}
|
|
});
|
|
|
|
it('nessuna pagina del gestionale nomina il database anagrafico riservato', () => {
|
|
for (const p of PAGINE_GESTIONALE) {
|
|
expect(leggi(p)).not.toMatch(/Identity/);
|
|
}
|
|
});
|
|
|
|
it('index.astro smista per ruolo: trainer e admin al gestionale, il cliente al proprio referto', () => {
|
|
const src = leggi('src/pages/longevity/index.astro');
|
|
expect(src).toMatch(/Astro\.redirect\(\s*['"`]\/longevity\/gestionale['"`]/);
|
|
expect(src).toMatch(/trainer/);
|
|
expect(src).toMatch(/admin/);
|
|
expect(src).toMatch(/Astro\.redirect\(\s*['"`]\/longevity\/io['"`]/);
|
|
});
|
|
|
|
it('la scheda cliente porta le azioni di verifica di una misura fuori range (#7) e il link per creare l\'accesso (#8)', () => {
|
|
const src = leggi('src/pages/longevity/gestionale/cliente/[code].astro');
|
|
expect(src).toMatch(/action=["']\/api\/longevity\/gestionale\/verifica["']/);
|
|
expect(src).toMatch(/name=["']misura_id["']/);
|
|
expect(src).toMatch(/value=["']confermata["']/);
|
|
expect(src).toMatch(/value=["']scartata["']/);
|
|
expect(src).toMatch(/\/longevity\/gestionale\/cliente\/\$\{cliente\.client_code\}\/accesso/);
|
|
});
|
|
|
|
it('la scheda cliente porta il link "Andamento" accanto a "Nuovo check-up" (#14)', () => {
|
|
const src = leggi('src/pages/longevity/gestionale/cliente/[code].astro');
|
|
expect(src).toMatch(/\/longevity\/gestionale\/cliente\/\$\{cliente\.client_code\}\/andamento/);
|
|
});
|
|
|
|
it('la pagina andamento del gestionale usa andamento.ts con trainer:true (#14): il trainer vede anche i grafici riservati', () => {
|
|
const src = leggi('src/pages/longevity/gestionale/cliente/[code]/andamento.astro');
|
|
expect(src).toMatch(/from ['"`].*lib\/longevity\/andamento['"`]/);
|
|
expect(src).toMatch(/andamentoTest\([^)]*trainer:\s*true/);
|
|
});
|
|
|
|
it('nessuna pagina longevity usa piu misureFuoriRange, rimossa da gestionale.ts a favore di misureDaVerificare (#7)', () => {
|
|
// Scandisce TUTTE le pagine sotto src/pages/longevity/, stesso criterio di
|
|
// referto.test.ts: un elenco scritto qui non protegge una pagina nuova.
|
|
const dir = join(process.cwd(), 'src/pages/longevity');
|
|
const pagine = (readdirSync(dir, { recursive: true }) as string[]).filter((f) => f.endsWith('.astro'));
|
|
expect(pagine.length).toBeGreaterThan(0);
|
|
for (const f of pagine) {
|
|
expect(readFileSync(join(dir, f), 'utf8'), `src/pages/longevity/${f}`).not.toMatch(/misureFuoriRange/);
|
|
}
|
|
});
|
|
});
|