6301f521fd
Campus, Stress Index e Longevity escono dal sito e diventano apps.insanitylab.it, con utenti, sessioni, login e database propri. Deciso da Adriano il 03/09/2026: una repo sola, due applicazioni autosufficienti, cosi' che ognuna resti estraibile. Misura che ha guidato il taglio: le tre aree condividevano col sito SOLO l'identita' - ~170 righe fra auth.ts, middleware.ts e safe-next.ts. Layout, librerie e dati erano gia' disgiunti. Lato piattaforme: - ruoli piattaforme/cliente/trainer/admin, senza quelli redazionali - canAccessPath con default CHIUSO (nel sito era aperto: li' le rotte non elencate erano di chiunque fosse loggato, qui ogni sezione e' di qualcuno) - tutta l'app protetta, si elencano le eccezioni invece dei prefissi protetti - /api risponde 401/403, non un redirect verso una pagina di login - tre database distinti: utenti, dati clinici senza nomi, mappatura coi nomi Lato sito: - ROLES tornano i tre redazionali; via le regole e il login pubblico - il login del pannello RESPINGE un account emigrato invece di aprirgli una sessione: senza, landingFor lo mandava su /admin, il middleware glielo negava, e si formava un ciclo di redirect che sembra un guasto del sito - l'header non mostra piu' un menu delle piattaforme: un link che esce Verificato: sito 20 file / 146 test, piattaforme 26 file / 197 test, entrambi i build passano, e l'app girata davvero in locale - il ruolo piattaforme entra in Campus e viene rimbalzato da Longevity, il cliente il contrario. Non ancora in produzione: manca il record DNS di apps.insanitylab.it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
97 lines
5.1 KiB
TypeScript
97 lines
5.1 KiB
TypeScript
// tests/longevity/pagine.test.ts
|
|
import { describe, it, expect } from 'vitest';
|
|
import { readFileSync, existsSync } 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('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['"`]/);
|
|
});
|
|
});
|