Validatore di accessibilità WCAG che gira interamente in locale.
Dato un URL (o un PDF), produce un report HTML navigabile che spiega
successi, errori e warning — con le metriche di MAUVE++.
accval-pa carica la pagina in un browser headless (esegue il JavaScript, quindi valuta anche le SPA Angular/Vue/React), esegue axe-core sul DOM renderizzato e organizza i risultati secondo il modello di MAUVE++ (CNR-ISTI / AgID): quattro esiti, due metriche pesate, due viste. Tutto offline, nessun dato inviato a servizi esterni.
- 🔒 100% locale — nessun upload, nessun account, nessuna telemetria.
- 📄 Report autosufficiente — un singolo file HTML, apribile con doppio click, che resta accessibile e leggibile anche senza JavaScript.
- 👥 Due viste — Sviluppatore (selettori CSS, frammenti, come correggere) e Utente finale (linguaggio non tecnico).
- 📊 Metriche MAUVE++ — Accessibility Percentage pesata + Evaluation Completeness, con tabella per versione × livello (leggi un report 2.2/AAA come 2.1/AA, senza rieseguire).
- 🕸️ Scansione di sito — crawler sul dominio o via sitemap XML, con aggregazione trasparente.
- 📎 Validazione PDF — 7 tecniche PDF di MAUVE++, per documento singolo o per i PDF collegati alle pagine (metrica COMBINED).
- 🇮🇹 Pensato per la PA — allineato al target di legge WCAG 2.1 AA (EN 301 549 / AgID).
Approccio ibrido. MAUVE++ pubblica open source solo le definizioni delle linee guida (linguaggio LWGD, file
wcag21.xml), non il motore che le interpreta. Riscriverlo è un progetto a sé:accval-paadotta axe-core come motore e ne mantiene presentazione e scoring secondo MAUVE++.
- Installazione
- Uso
- Scansione di un sito
- Validazione dei PDF
- Il report
- Metriche
- Opzioni
- Interfaccia grafica
- Elenco di pagine scelte a mano
- Avanzamento leggibile da programma
- Limiti
- Sviluppo
- Come contribuire
- Licenze
Richiede Python 3.11+.
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
python -m playwright install chromiumIl primo comando crea un ambiente isolato, il secondo installa il pacchetto e le dipendenze
(incluse pikepdf/pypdf per i PDF), il terzo scarica il browser Chromium usato per il
rendering (~180 MB).
L'eseguibile va prodotto sul sistema di destinazione (PyInstaller non fa cross-compilazione):
pip install -e ".[packaging]"
pyinstaller --clean --noconfirm accval-pa.spec # risultato in dist/accval-pa/Il programma così costruito, avviato senza argomenti, apre l'interfaccia nel browser; richiamato con argomenti si comporta come la riga di comando. axe-core e i template sono inclusi; Chromium non è incluso e viene scaricato al primo avvio (~270 MB).
In ambienti offline o dietro proxy si può pre-installare Chromium a parte e impostare
ACCVAL_SKIP_BROWSER_SETUP=1 per saltare il download automatico all'avvio.
accval-pa https://example.comGenera report-example.com-<timestamp>.html nella directory corrente.
# report in un percorso specifico, con export JSON ed EARL
accval-pa https://example.com -o report.html --json dati.json --earl dati.earl.json
# limita la valutazione al livello AA e apri il report al termine
accval-pa https://example.com --level AA --open
# SPA lenta: più tempo di attesa e frammenti HTML completi
accval-pa https://app.example.com --timeout 60 --settle-ms 2000 --snippet-context outer
# aggiunge un ritaglio evidenziato di ogni elemento segnalato
accval-pa https://example.com --screenshot-elements
# valuta un singolo documento PDF (file locale o URL con estensione .pdf)
accval-pa documento.pdf -o report-pdf.html# segue i collegamenti a partire dall'URL, restando sul dominio
accval-pa https://www.esempio.it --crawl --max-pages 50 -o report-sito/
# usa una sitemap XML invece del crawler
accval-pa https://www.esempio.it --sitemap https://www.esempio.it/sitemap.xml -o report-sito/Produce una cartella navigabile:
report-sito/
├── index.html metriche aggregate + riepilogo di ogni pagina
└── pagine/
├── 0001-home.html report di dettaglio, identico al caso singola pagina
└── 0002-servizi.html
Con --screenshot-elements ogni report di pagina è affiancato da una cartella
<nome-report>-immagini/ con un PNG per elemento segnalato, mostrato nel report
accanto al selettore e al frammento HTML:
pagine/
├── 0001-home.html
└── 0001-home-immagini/
├── image-alt-001.png
└── color-contrast-001.png
L'indice elenca le pagine dalla più problematica, con Accessibility, Completeness, errori e warning per riga, e collega il report di dettaglio. Ogni report di pagina resta autosufficiente e apribile da solo; l'intera cartella è spostabile.
Il crawler resta sul dominio di partenza (www incluso), rispetta robots.txt,
non segue risorse non HTML per la navigazione (immagini, archivi) e deduplica gli
URL equivalenti (/p, /p/, /p#sezione). I collegamenti sono estratti dal
DOM renderizzato, quindi funziona anche sulle SPA che costruiscono la
navigazione via JavaScript. I PDF collegati vengono invece scaricati e
validati (vedi sotto).
Con --json e --earl in modalità sito si ottengono un JSON aggregato e un
documento EARL con un soggetto di test per pagina (e per PDF validato).
La metrica di sito è la media aritmetica delle metriche di pagina (macro-average): ogni pagina pesa uguale, indipendentemente da quante tecniche produce.
Pagina A: 100,0% (10 tecniche decise)
Pagina B: 50,0% ( 2 tecniche decise)
Metrica del sito = (100,0 + 50,0) / 2 = 75,0%
La scelta è deliberata: aggregando invece sull'unione delle tecniche (micro-average) lo stesso caso darebbe 91,7%, perché la pagina grande mascherebbe quella piccola. Le pagine non valutabili sono contate a parte e non entrano nella media, per non gonfiare il punteggio.
Il default è WCAG 2.1. Con --wcag 2.2 si aggiungono i tag wcag22* e si
abilita target-size, che axe-core spedisce disattivata: è l'unica regola
automatica di WCAG 2.2 disponibile e copre il solo SC 2.5.8 (dimensione minima
del target). Gli altri otto criteri introdotti da 2.2 — 2.4.11, 2.4.12, 2.4.13,
2.5.7, 3.2.6, 3.3.7, 3.3.8, 3.3.9 — non sono verificabili automaticamente e
restano fuori dal calcolo, come già accade per tutti i criteri non
automatizzabili.
Il default resta 2.1 di proposito: cambiarlo sposterebbe i punteggi rendendoli
non confrontabili con i report già emessi. SC 4.1.1 (rimosso in WCAG 2.2) non è
comunque valutato: in axe-core porta il tag wcag2a-obsolete.
| Opzione | Default | Descrizione |
|---|---|---|
-o, --output PATH |
./report-<host>-<timestamp>[.html] |
File HTML; in modalità sito è la cartella di destinazione |
--json PATH |
— | Salva anche il JSON grezzo |
--earl PATH |
— | Salva anche l'export EARL (JSON-LD) |
--level {A,AA,AAA} |
AAA |
Livello massimo da valutare (cumulativo) |
--wcag {2.0,2.1,2.2} |
2.1 |
Versione WCAG da valutare (cumulativa) |
--timeout SECONDS |
30 |
Timeout di rendering |
--wait {load,domcontentloaded,networkidle} |
networkidle |
Strategia di attesa |
--viewport WxH |
1280x800 |
Viewport del browser |
--settle-ms N |
500 |
Attesa aggiuntiva per stabilizzare le SPA |
--snippet-context {tag,element,outer} |
tag |
Ampiezza del frammento HTML mostrato |
--snippet-max-chars N |
600 |
Tetto caratteri per element/outer |
--disable-cors |
off | Disattiva la same-origin policy del browser: valuta un frontend locale che legge da un backend su un altro host. Solo in sviluppo |
--screenshot |
off | Screenshot full-page incorporato nel report |
--screenshot-elements |
off | Ritaglio evidenziato di ogni elemento in errore o da verificare, come PNG esterno |
--max-element-shots N |
100 |
Tetto di ritagli per pagina |
--gui |
off | Avvia l'interfaccia grafica nel browser (vedi Interfaccia grafica) |
--gui-port PORTA |
libera | Porta dell'interfaccia grafica |
--gui-no-browser |
off | Con --gui, non apre il browser: stampa solo l'indirizzo |
--open |
off | Apre il report nel browser al termine |
--progress-json |
off | Emette l'avanzamento su stdout come righe JSON invece del testo, per l'uso da altri programmi (vedi Avanzamento leggibile da programma) |
-v, --verbose |
off | Log dettagliato |
Scansione multi-pagina:
| Opzione | Default | Descrizione |
|---|---|---|
--crawl |
off | Attiva il crawler multi-pagina |
--max-pages N |
100 (con --urls-file: tutte) |
Limite di pagine da valutare |
--sitemap URL |
— | Usa una sitemap XML invece del crawler |
--urls-file PATH |
— | Valuta l'elenco di URL nel file, uno per riga (- legge da stdin). Vedi Elenco di pagine scelte a mano |
--same-path |
off | Resta nel sottoalbero di path dell'URL di partenza (da /sito segue solo /sito/...) |
--exclude REGEX |
— | Esclude gli URL corrispondenti (ripetibile) |
--ignore-robots |
off | Non applica robots.txt |
--no-pdf |
off | Non validare i PDF collegati dalle pagine |
--max-pdfs N |
50 |
Numero massimo di PDF collegati da validare |
Codici di uscita: 0 ok, 1 errore di rendering/rete (o nessuna pagina
valutabile), 2 errore di configurazione.
Per chi non usa la riga di comando:
$ accval-pa --gui
Interfaccia grafica su http://127.0.0.1:54702/?token=besHwTt3Xh...
I report saranno scritti in /percorso/accval-pa-report
Premi Ctrl+C per chiudere.Si apre una pagina con un modulo: si sceglie se valutare una pagina, un sito o un elenco di indirizzi, si incolla l'URL e si avvia. L'avanzamento compare passo per passo e a fine valutazione un collegamento apre il report. Le opzioni avanzate (livello, versione WCAG, immagini, PDF) stanno in una sezione a scomparsa, sui valori predefiniti della riga di comando.
La pagina segue lo stesso principio del report: funziona anche con JavaScript disattivato — il modulo si invia normalmente e la pagina di stato si aggiorna da sola. Ed è essa stessa conforme: supera axe a livello A/AA, come il report che produce.
Il comando avvia un server e apre il browser predefinito. La valutazione non
gira nel server: viene eseguita accval-pa stessa come sottoprocesso, con
--progress-json, e il server ne legge lo stdout mentre procede. Così un
guasto di Chromium non porta giù l'interfaccia, «Annulla» termina davvero il
processo e il browser che ha avviato, e il motore resta uno solo.
Il server è in ascolto mentre l'utente naviga altrove, e su richiesta apre indirizzi arbitrari e scrive file: qualunque pagina web aperta nello stesso browser potrebbe tentare di usarlo come tramite. Le difese sono quattro e indipendenti:
| Difesa | Cosa impedisce |
|---|---|
Ascolto sul solo 127.0.0.1, porta effimera |
raggiungibilità dalla rete |
| Token per sessione nell'URL, confrontato a tempo costante | uso da parte di altre pagine |
Host limitato a 127.0.0.1/localhost con la porta in uso |
DNS rebinding |
Origin limitata alle stesse origini, API solo application/json |
invio da form su siti esterni |
Il token vive quanto il processo: alla riapertura porta e token sono altri.
Tutte richiedono il token, in X-Accval-Token o come parametro token.
| Metodo | Percorso | Effetto |
|---|---|---|
GET |
/ |
la pagina dell'interfaccia |
GET |
/stato |
la pagina con lo stato dentro (percorso senza JavaScript) |
POST |
/api/run |
avvia una valutazione (202, oppure 409 se ce n'è già una) |
POST |
/run |
avvia dal modulo HTML (x-www-form-urlencoded, 303 verso /stato) |
GET |
/api/status?since=N |
stato ed eventi dal numero N in poi |
POST |
/api/cancel |
termina la valutazione in corso e i suoi processi figli |
GET |
/report/… |
apre un report prodotto (percorso verificato dentro la cartella di lavoro) |
Il corpo di /api/run indica modalità, indirizzo e opzioni:
{
"mode": "site",
"url": "https://esempio.it",
"output": "comune-di-prova",
"options": { "level": "AA", "max_pages": 30 }
}mode vale single, site o list (con urls, un indirizzo per riga). Le
opzioni sono una tabella chiusa: un nome non previsto è un errore, non un
argomento passato alla riga di comando. output è un nome di cartella, mai un
percorso: il report resta dentro la cartella di lavoro.
Quando le pagine da valutare sono note — un campione concordato, le pagine citate in una dichiarazione di accessibilità, quelle corrette dopo un audit — si indicano una per riga in un file, senza far scoprire nulla al crawler:
# campione per la verifica di conformità
https://esempio.it/
https://esempio.it/servizi
https://esempio.it/amministrazione-trasparente
$ accval-pa --urls-file pagine.txt -o report
Elenco: 3 pagine da valutare.
[1/3] 93.8% 1 errori https://esempio.it/
...L'elenco si può passare anche da standard input con --urls-file -.
Rispetto alla scansione cambia il perimetro, non la valutazione: le metriche restano le stesse (macro-average sulle pagine) e i report delle tre modalità sono confrontabili fra loro.
- Nessun confine di dominio o di path: le pagine le hai scelte tu, anche su
siti diversi. Per la stessa ragione
--same-pathed--excludenon sono ammessi insieme a--urls-file— filtrare un elenco esplicito sarebbe una contraddizione, e il tool lo dice invece di ignorarli. robots.txtresta applicato, come nelle altre modalità, con unrobots.txtdistinto per ogni host attraversato. Gli URL vietati sono saltati e compaiono nel report con il motivo;--ignore-robotsli include.- Niente sparisce in silenzio. Righe vuote e righe che iniziano con
#sono ignorate; tutto il resto che non viene valutato è dichiarato con il perché: URL malformati, schemi diversi da http/https, risorse non HTML, PDF (che si validano a parte), duplicati come/pe/p/, pagine escluse darobots.txto oltre--max-pages. - I duplicati e le pagine tagliate da
--max-pagessono segnalati ma non contati fra le pagine non valutabili: non sono fallimenti.
Con --progress-json l'avanzamento esce su stdout come
JSON Lines: una riga JSON per evento, svuotata
subito, così chi legge il flusso vede la scansione procedere in tempo reale.
I messaggi per l'operatore e i log restano su stderr: stdout contiene solo
eventi.
$ accval-pa https://example.com/ --crawl --max-pages 3 -o report --progress-json
{"event": "run_started", "mode": "site", "url": "https://example.com/", "total": 3, "discovery": "crawl"}
{"event": "page_done", "index": 1, "total": 3, "url": "https://example.com/", "accessibility": 93.75, "count_error": 1}
{"event": "artifact", "kind": "site_index", "path": "/percorso/report/index.html"}
{"event": "site_totals", "accessibility": 91.85, "completeness": 100.0, "pages_evaluated": 3, ...}| Evento | Quando | Campi principali |
|---|---|---|
run_started |
avvio | mode (single/site/pdf), url, total, discovery (crawl/sitemap/list) |
url_skipped |
URL dell'elenco scartato prima di aprirlo | url, reason |
page_done |
pagina valutata | index, total, url, accessibility, count_error |
page_failed |
pagina non valutabile | index, total, url, error |
pdf_phase_started |
inizio validazione dei PDF collegati | total |
pdf_done / pdf_failed |
esito di un PDF | come sopra |
artifact |
file prodotto | kind (report, json, earl, site_index, pages_dir, site_json), path |
page_totals / pdf_totals / site_totals |
riepilogo finale | metriche e conteggi |
run_error |
errore che interrompe la valutazione | message, exit_code |
I valori numerici non sono arrotondati: l'arrotondamento a una cifra è solo presentazione dell'output testuale. I nomi degli eventi e delle chiavi sono un contratto: non cambiano senza una nota nel changelog.
Oltre alle pagine web, accval-pa valuta l'accessibilità dei documenti PDF,
replicando il sottoinsieme di 7 tecniche adottato da MAUVE++ (le PDF* sono
tecniche PDF ufficiali di WCAG, le MPDF* best practice MAUVE++):
| Tecnica | Controllo | Criteri |
|---|---|---|
| MPDF1 | struttura logica marcata (PDF tagged) | 1.3.1 |
| PDF9 | intestazioni marcate come heading | 1.3.1, 2.4.1 |
| PDF18 | titolo del documento impostato | 2.4.2 |
| PDF16 | lingua predefinita dichiarata | 3.1.1 |
| MPDF3 | estrazione di testo/immagini consentita | 1.1.1 |
| MPDF2 | PDF ricercabile (non solo immagine) | 1.1.1 |
| PDF3 | ordine di lettura e tabulazione | 1.3.2, 2.1.1, 2.4.3 |
Il motore è puro Python (pikepdf per struttura, metadati e permessi; pypdf
per l'estrazione del testo): nessun runtime esterno. L'ordine di lettura (PDF3)
non è decidibile automaticamente e resta un warning, come per il web.
# PDF singolo, con export
accval-pa documento.pdf -o report-pdf.html --json dati.json --earl dati.earl.jsonIn modalità sito i PDF collegati dalle pagine vengono scaricati e validati
automaticamente (disattivabile con --no-pdf, limitabile con --max-pdfs).
L'indice mostra una sezione dedicata ai PDF e la metrica COMBINED di MAUVE++:
COMBINED (pagina) = (Accessibility% della pagina + Accessibility% media dei suoi PDF) / 2
COMBINED (sito) = media per pagina dei COMBINED
Singolo file HTML con CSS e JS inline: si apre con doppio click, offline, senza alcuna dipendenza esterna a runtime.
- Due viste commutabili: Sviluppatore (selettori CSS, frammenti HTML, indicazioni di correzione) e Utente finale (linguaggio non tecnico).
- Filtri client-side per esito, livello di conformità, versione WCAG, criterio di successo, più ricerca libera.
- Metriche per versione e livello WCAG: una tabella espone Accessibility ed
Evaluation Completeness per ogni perimetro cumulativo (versione × livello). Così
un report emesso a
--wcag 2.2 --level AAAsi può leggere anche con le metriche di un perimetro più ristretto (es. WCAG 2.1 AA, riferimento di legge per la PA italiana) e confrontarle con MAUVE++, senza rieseguire la valutazione. La tabella funziona anche con JavaScript disattivato. L'indice di sito riporta la stessa matrice in media (macro-average). - Raggruppamento per principio WCAG → criterio di successo → tecnica.
- Il report è esso stesso accessibile: superato da axe senza violazioni A/AA, navigabile da tastiera, e resta completo anche con JavaScript disattivato (in tal caso senza filtri, con tutto espanso).
Replicano la metodologia pubblica di MAUVE++. Pesi per livello di conformità,
applicati sia ai successi sia agli errori: A = 1,0, AA = 0,6, AAA = 0,2.
Accessibility Percentage — peso delle tecniche superate sul peso di tutte quelle per cui il tool ha emesso un verdetto certo. Warning e Non applicabili sono esclusi dal denominatore.
100 × Σ peso(pass) / Σ peso(pass ∪ error)
Evaluation Completeness — percentuale di tecniche applicabili per cui il tool ha saputo decidere, cioè escludendo i warning.
100 × #(pass ∪ error) / #(pass ∪ error ∪ warning)
Entrambe valgono 100 quando il denominatore è zero. Il report espone anche i conteggi grezzi per esito e per livello, per rendere verificabile il calcolo.
Ogni regola axe è trattata come una tecnica nel senso di MAUVE++.
| Esito MAUVE++ | EARL | Collezione axe | Significato |
|---|---|---|---|
| Pass | earl:passed |
passes |
Tecnica soddisfatta |
| Error | earl:failed |
violations |
Tecnica violata |
| Warning | earl:cantTell |
incomplete |
Serve verifica manuale |
| Not Applicable | earl:inapplicable |
inapplicable |
Non pertinente alla pagina |
Da comunicare esplicitamente, come fa MAUVE++:
- La validazione automatica non intercetta tutti i problemi. Molti criteri WCAG richiedono giudizio umano e nessun tool può deciderli. Un'Accessibility Percentage alta non equivale alla conformità.
- I warning (cannot tell) sono controlli che il motore non sa risolvere: vanno verificati a mano e non sono successi.
- La Evaluation Completeness misura quanto il tool ha saputo decidere, non quanto la pagina sia accessibile.
- La media di sito non è un indice di conformità: con
--max-pagessi valuta un campione, non l'intero sito, e le pagine sono quelle raggiungibili dai collegamenti (o elencate in sitemap). - Non gestisce autenticazione sui siti target. La validazione dei PDF copre il sottoinsieme di 7 tecniche di MAUVE++ (proprietà strutturali): non sostituisce una verifica PDF/UA completa né il giudizio umano sull'ordine di lettura.
pytest # test unitari (veloci, senza browser)
pytest -m browser # test end-to-end (richiedono Chromium)
pytest -m "browser or not browser" # tuttoI test end-to-end usano le fixture in tests/fixtures/ caricate via file://
e verificano anche l'accessibilità del report stesso.
src/accval_pa/
├── cli.py # entrypoint e wiring
├── renderer.py # Playwright: caricamento e stabilizzazione della pagina
├── engine.py # iniezione ed esecuzione di axe-core, snippet HTML
├── shots.py # ritagli PNG degli elementi segnalati
├── mapping.py # axe JSON → TechniqueResult
├── pdf.py # validazione PDF (7 tecniche MAUVE++) con pikepdf/pypdf
├── scoring.py # metriche di pagina, matrice versione×livello, aggregazione di sito, COMBINED
├── wcag.py # criteri WCAG 2.1 + 2.2, tecniche PDF, conversioni, livelli, versioni
├── crawler.py # BFS sul dominio, robots.txt, sitemap XML, elenco di URL, link PDF
├── models.py # dataclass del modello dati
├── progress.py # eventi di avanzamento: presentazione testuale e JSON Lines
└── report/
├── html.py # report di pagina: view model e rendering Jinja2
├── pdf.py # report di un documento PDF
├── site.py # indice di sito multi-pagina (con sezione PDF e COMBINED)
├── earl.py # export EARL/JSON-LD
└── templates/
axe-core è versionato nel repository (assets/axe/axe.min.js, v4.12.1)
invece di essere scaricato a runtime, e la sua versione è riportata nel report.
Per usare una copia diversa: variabile d'ambiente ACCVAL_AXE_PATH.
I contributi sono benvenuti. Prima di aprire una issue o una pull request:
- Leggi le linee guida per contribuire e il codice di condotta.
- Per vulnerabilità di sicurezza, segui la security policy (non aprire issue pubbliche).
- Il registro delle modifiche è in CHANGELOG.md.
In breve: apri un ambiente isolato, mantieni ruff pulito e i test verdi
(ruff check src tests e pytest), scrivi test per ogni nuovo comportamento e usa
commit in stile Conventional Commits.
accval-pa è rilasciato sotto MPL-2.0, compatibile con axe-core (MPL-2.0),
di cui incorpora una copia in assets/axe/.
L'eventuale modalità LWGD (uso diretto del wcag21.xml di MAUVE++, GPL-3.0)
introdurrebbe copyleft forte: va tenuta isolata e documentata separatamente.