Skip to content
fdidonatoPublic

About

Validatore di accessibilita WCAG 2.1 e WCAG 2.2 locale, con report HTML navigabile secondo il modello MAUVE++

Resources

Code of conduct

Contributing

Security policy

Stars

10 stars

Watchers

0 watching

Forks

accval-pa

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++.

License: MPL 2.0 Python WCAG Tests Ruff PDF


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.

Perché accval-pa

  • 🔒 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-pa adotta axe-core come motore e ne mantiene presentazione e scoring secondo MAUVE++.

Indice

Installazione

Richiede Python 3.11+.

python -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
python -m playwright install chromium

Il 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).

Eseguibile autonomo (senza Python)

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.

Uso

accval-pa https://example.com

Genera 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

Scansione di un sito

# 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).

Come si aggregano le metriche

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.

Versione WCAG

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.

Opzioni

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.

Interfaccia grafica

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.

Perché è protetto

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.

API

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.

Elenco di pagine scelte a mano

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-path ed --exclude non sono ammessi insieme a --urls-file — filtrare un elenco esplicito sarebbe una contraddizione, e il tool lo dice invece di ignorarli.
  • robots.txt resta applicato, come nelle altre modalità, con un robots.txt distinto per ogni host attraversato. Gli URL vietati sono saltati e compaiono nel report con il motivo; --ignore-robots li 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 /p e /p/, pagine escluse da robots.txt o oltre --max-pages.
  • I duplicati e le pagine tagliate da --max-pages sono segnalati ma non contati fra le pagine non valutabili: non sono fallimenti.

Avanzamento leggibile da programma

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.

Validazione dei PDF

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.json

In 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

Il report

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 AAA si 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).

Metriche

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.

Modello dei risultati

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

Limiti

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-pages si 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.

Sviluppo

pytest                 # test unitari (veloci, senza browser)
pytest -m browser      # test end-to-end (richiedono Chromium)
pytest -m "browser or not browser"   # tutto

I test end-to-end usano le fixture in tests/fixtures/ caricate via file:// e verificano anche l'accessibilità del report stesso.

Struttura

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/

Riproducibilità

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.

Come contribuire

I contributi sono benvenuti. Prima di aprire una issue o una pull request:

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.

Licenze

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.

About

Validatore di accessibilita WCAG 2.1 e WCAG 2.2 locale, con report HTML navigabile secondo il modello MAUVE++

Resources

Code of conduct

Contributing

Security policy

Stars

10 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages