Spaces:
Running
feat(sprint-A6): WCAG niveau A bloquant — skip-link, canvas a11y, scope=col
Browse filesSprint A6 — Accessibilité WCAG niveau A (3 PJ, items B-9, B-10, m-3, m-4).
Items résolus :
- B-10 (WCAG 2.4.1 Bypass Blocks) : skip-to-content link
- <a class="skip-link" href="#main"> en premier enfant focusable du body
- .skip-link CSS : caché hors :focus, top:0 + outline:3px solid #fbbf24 au focus
- id="main" + role="main" sur le <main> (cible du lien)
- Libellé i18n FR "Aller au contenu" / EN "Skip to content"
- B-9 (WCAG 1.1.1 Non-text Content) : graphiques Chart.js accessibles
- role="img" + aria-label posés statiquement sur les 11 <canvas>
Chart.js (cer-hist, radar, cer-doc, duration, quality-cer,
taxonomy, reliability, bootstrap-ci, gini-cer, ratio-anchor,
pareto-chart)
- data-a11y-label sur chaque canvas pour permettre au JS d'enrichir
- Helper JS attachChartA11y() : génère pour chaque canvas un bouton
"Voir les données" + une <table class="chart-data-table"> jumelle
avec scope="col"/scope="row", reliée par aria-describedby. Génération
paresseuse au premier clic via _populateChartDataTable() qui
construit le tableau depuis chartInstances[id].data.
- showView() ré-attache les a11y aux charts qui s'instancient
paresseusement au switch de vue.
- m-3 : bouton "Réinitialiser" du bandeau d'exclusion globale passe
à data-i18n="reset_all" (FR "Réinitialiser" / EN "Reset").
- m-4 : scope="col" sur tous les <th> des tableaux.
- 16 <th> dans view_ranking.html
- 53 <th> dans 23 modules de rendu Python (ner, calibration,
philological, levers, throughput, longitudinal, etc.)
- Patch Python utilise scope=\"col\" (escaped quotes) pour ne pas
casser les délimiteurs de f-strings.
i18n FR/EN :
- Nouvelles clés : skip_to_content, reset_all, view_data, hide_data,
chart_no_data, chart_data_caption (× 2 langues = 12 clés ajoutées).
Tests : tests/report/test_a11y_level_a.py (13 cas) qui valident :
- Présence du skip-link en tête de body avant <nav>
- aria-label + role="img" sur tous les canvas Chart.js
- scope="col" sur les <th> rendus (regex strict, exclut <thead>
et le contenu inline des <script>/<style>)
- data-i18n="reset_all" sur le bouton Reset
- Libellés a11y bilingues (FR + EN distincts)
- Méta-test smoke des 7 marqueurs WCAG niveau A
Tests totaux : 3636 passed, 3 skipped, 4 deselected (network), 0 failed.
Coverage : 86.82% (plancher 85% maintenu).
- picarones/report/calibration_render.py +1 -1
- picarones/report/comparison.py +6 -6
- picarones/report/error_absorption_render.py +1 -1
- picarones/report/i18n/en.json +7 -1
- picarones/report/i18n/fr.json +7 -1
- picarones/report/image_predictive_render.py +7 -7
- picarones/report/incremental_comparison_render.py +1 -1
- picarones/report/inter_engine_render.py +3 -3
- picarones/report/lexical_modernization_render.py +1 -1
- picarones/report/longitudinal_render.py +1 -1
- picarones/report/module_audit_render.py +1 -1
- picarones/report/multirun_stability_render.py +1 -1
- picarones/report/ner_render.py +4 -4
- picarones/report/numerical_sequences_render.py +3 -3
- picarones/report/philological_render.py +7 -7
- picarones/report/pipeline_render.py +3 -3
- picarones/report/readability_render.py +1 -1
- picarones/report/robustness_projection_render.py +2 -2
- picarones/report/searchability_render.py +1 -1
- picarones/report/specialization_render.py +1 -1
- picarones/report/stratification_render.py +1 -1
- picarones/report/taxonomy_comparison_render.py +3 -3
- picarones/report/taxonomy_cooccurrence_render.py +2 -2
- picarones/report/templates/_app.js +119 -1
- picarones/report/templates/_header.html +10 -3
- picarones/report/templates/_styles.css +74 -0
- picarones/report/templates/view_analyses.html +11 -11
- picarones/report/templates/view_ranking.html +16 -16
- picarones/report/throughput_render.py +1 -1
- picarones/report/worst_lines_render.py +1 -1
- tests/report/test_a11y_level_a.py +237 -0
|
@@ -85,7 +85,7 @@ def build_calibration_summary_html(
|
|
| 85 |
for hdr in (engine_label, ece_label, mce_label,
|
| 86 |
acc_label, conf_label, n_label, docs_label):
|
| 87 |
parts.append(
|
| 88 |
-
f'<th style="padding:.3rem .5rem;text-align:left;'
|
| 89 |
f'border-bottom:1px solid var(--border);font-weight:600">'
|
| 90 |
f'{_e(hdr)}</th>'
|
| 91 |
)
|
|
|
|
| 85 |
for hdr in (engine_label, ece_label, mce_label,
|
| 86 |
acc_label, conf_label, n_label, docs_label):
|
| 87 |
parts.append(
|
| 88 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 89 |
f'border-bottom:1px solid var(--border);font-weight:600">'
|
| 90 |
f'{_e(hdr)}</th>'
|
| 91 |
)
|
|
@@ -335,12 +335,12 @@ _COMPARISON_TEMPLATE = """<!DOCTYPE html>
|
|
| 335 |
<table>
|
| 336 |
<thead>
|
| 337 |
<tr>
|
| 338 |
-
<th>Moteur</th>
|
| 339 |
-
<th class="num">CER A</th>
|
| 340 |
-
<th class="num">CER B</th>
|
| 341 |
-
<th class="num">Δ CER</th>
|
| 342 |
-
<th class="num">Docs A → B</th>
|
| 343 |
-
<th>État</th>
|
| 344 |
</tr>
|
| 345 |
</thead>
|
| 346 |
<tbody>
|
|
|
|
| 335 |
<table>
|
| 336 |
<thead>
|
| 337 |
<tr>
|
| 338 |
+
<th scope=\"col\">Moteur</th>
|
| 339 |
+
<th scope=\"col\" class="num">CER A</th>
|
| 340 |
+
<th scope=\"col\" class="num">CER B</th>
|
| 341 |
+
<th scope=\"col\" class="num">Δ CER</th>
|
| 342 |
+
<th scope=\"col\" class="num">Docs A → B</th>
|
| 343 |
+
<th scope=\"col\">État</th>
|
| 344 |
</tr>
|
| 345 |
</thead>
|
| 346 |
<tbody>
|
|
@@ -170,7 +170,7 @@ def build_error_absorption_html(
|
|
| 170 |
h_corrected, h_introduced, h_corr_rate,
|
| 171 |
h_intro_rate, h_net, h_sample):
|
| 172 |
parts.append(
|
| 173 |
-
f'<th style="padding:.4rem .6rem;text-align:left;'
|
| 174 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 175 |
f'{_e(col)}</th>'
|
| 176 |
)
|
|
|
|
| 170 |
h_corrected, h_introduced, h_corr_rate,
|
| 171 |
h_intro_rate, h_net, h_sample):
|
| 172 |
parts.append(
|
| 173 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 174 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 175 |
f'{_e(col)}</th>'
|
| 176 |
)
|
|
@@ -408,5 +408,11 @@
|
|
| 408 |
"audit_license": "Licence",
|
| 409 |
"audit_io": "Input → output",
|
| 410 |
"audit_citation": "Citation",
|
| 411 |
-
"audit_homepage": "Project page"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
}
|
|
|
|
| 408 |
"audit_license": "Licence",
|
| 409 |
"audit_io": "Input → output",
|
| 410 |
"audit_citation": "Citation",
|
| 411 |
+
"audit_homepage": "Project page",
|
| 412 |
+
"skip_to_content": "Skip to content",
|
| 413 |
+
"reset_all": "Reset",
|
| 414 |
+
"view_data": "View data",
|
| 415 |
+
"hide_data": "Hide data",
|
| 416 |
+
"chart_no_data": "No data available",
|
| 417 |
+
"chart_data_caption": "Chart data"
|
| 418 |
}
|
|
@@ -408,5 +408,11 @@
|
|
| 408 |
"audit_license": "Licence",
|
| 409 |
"audit_io": "Entrée → sortie",
|
| 410 |
"audit_citation": "Citation",
|
| 411 |
-
"audit_homepage": "Page projet"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
}
|
|
|
|
| 408 |
"audit_license": "Licence",
|
| 409 |
"audit_io": "Entrée → sortie",
|
| 410 |
"audit_citation": "Citation",
|
| 411 |
+
"audit_homepage": "Page projet",
|
| 412 |
+
"skip_to_content": "Aller au contenu",
|
| 413 |
+
"reset_all": "Réinitialiser",
|
| 414 |
+
"view_data": "Voir les données",
|
| 415 |
+
"hide_data": "Masquer les données",
|
| 416 |
+
"chart_no_data": "Aucune donnée disponible",
|
| 417 |
+
"chart_data_caption": "Données du graphique"
|
| 418 |
}
|
|
@@ -86,17 +86,17 @@ def _render_complexity_block(
|
|
| 86 |
'<table style="border-collapse:collapse;width:100%;'
|
| 87 |
'font-size:.9rem;margin-bottom:.8rem">'
|
| 88 |
f'<thead><tr>'
|
| 89 |
-
f'<th style="padding:.4rem .6rem;text-align:right;'
|
| 90 |
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_mean)}</th>'
|
| 91 |
-
f'<th style="padding:.4rem .6rem;text-align:right;'
|
| 92 |
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_median)}</th>'
|
| 93 |
-
f'<th style="padding:.4rem .6rem;text-align:right;'
|
| 94 |
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_min)}</th>'
|
| 95 |
-
f'<th style="padding:.4rem .6rem;text-align:right;'
|
| 96 |
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_max)}</th>'
|
| 97 |
-
f'<th style="padding:.4rem .6rem;text-align:right;'
|
| 98 |
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_stdev)}</th>'
|
| 99 |
-
f'<th style="padding:.4rem .6rem;text-align:right;'
|
| 100 |
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_docs)}</th>'
|
| 101 |
f'</tr></thead>'
|
| 102 |
f'<tbody><tr>'
|
|
@@ -143,7 +143,7 @@ def _render_homogeneity_block(
|
|
| 143 |
]
|
| 144 |
for col in (h_feat, h_mean, h_stdev, h_norm):
|
| 145 |
parts.append(
|
| 146 |
-
f'<th style="padding:.4rem .6rem;text-align:left;'
|
| 147 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 148 |
f'{_e(col)}</th>'
|
| 149 |
)
|
|
|
|
| 86 |
'<table style="border-collapse:collapse;width:100%;'
|
| 87 |
'font-size:.9rem;margin-bottom:.8rem">'
|
| 88 |
f'<thead><tr>'
|
| 89 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 90 |
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_mean)}</th>'
|
| 91 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 92 |
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_median)}</th>'
|
| 93 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 94 |
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_min)}</th>'
|
| 95 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 96 |
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_max)}</th>'
|
| 97 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 98 |
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_stdev)}</th>'
|
| 99 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 100 |
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_docs)}</th>'
|
| 101 |
f'</tr></thead>'
|
| 102 |
f'<tbody><tr>'
|
|
|
|
| 143 |
]
|
| 144 |
for col in (h_feat, h_mean, h_stdev, h_norm):
|
| 145 |
parts.append(
|
| 146 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 147 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 148 |
f'{_e(col)}</th>'
|
| 149 |
)
|
|
@@ -143,7 +143,7 @@ def build_incremental_comparison_html(
|
|
| 143 |
]
|
| 144 |
for col in (h_value, h_mean, h_stdev, h_rank, h_n_obs):
|
| 145 |
parts.append(
|
| 146 |
-
f'<th style="padding:.4rem .6rem;text-align:left;'
|
| 147 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 148 |
f'{_e(col)}</th>'
|
| 149 |
)
|
|
|
|
| 143 |
]
|
| 144 |
for col in (h_value, h_mean, h_stdev, h_rank, h_n_obs):
|
| 145 |
parts.append(
|
| 146 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 147 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 148 |
f'{_e(col)}</th>'
|
| 149 |
)
|
|
@@ -102,10 +102,10 @@ def build_divergence_matrix_html(
|
|
| 102 |
)
|
| 103 |
parts.append('<table class="divergence-matrix" style="border-collapse:collapse;font-size:.8rem">')
|
| 104 |
# En-tête
|
| 105 |
-
parts.append("<thead><tr><th></th>")
|
| 106 |
for b in engines:
|
| 107 |
parts.append(
|
| 108 |
-
f'<th style="padding:.3rem .5rem;text-align:center;'
|
| 109 |
f'border-bottom:1px solid var(--border)">{_e(b)}</th>'
|
| 110 |
)
|
| 111 |
parts.append("</tr></thead>")
|
|
@@ -114,7 +114,7 @@ def build_divergence_matrix_html(
|
|
| 114 |
for a in engines:
|
| 115 |
parts.append("<tr>")
|
| 116 |
parts.append(
|
| 117 |
-
f'<th style="padding:.3rem .5rem;text-align:right;'
|
| 118 |
f'border-right:1px solid var(--border);font-weight:600">{_e(a)}</th>'
|
| 119 |
)
|
| 120 |
for b in engines:
|
|
|
|
| 102 |
)
|
| 103 |
parts.append('<table class="divergence-matrix" style="border-collapse:collapse;font-size:.8rem">')
|
| 104 |
# En-tête
|
| 105 |
+
parts.append("<thead><tr><th scope=\"col\"></th>")
|
| 106 |
for b in engines:
|
| 107 |
parts.append(
|
| 108 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:center;'
|
| 109 |
f'border-bottom:1px solid var(--border)">{_e(b)}</th>'
|
| 110 |
)
|
| 111 |
parts.append("</tr></thead>")
|
|
|
|
| 114 |
for a in engines:
|
| 115 |
parts.append("<tr>")
|
| 116 |
parts.append(
|
| 117 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:right;'
|
| 118 |
f'border-right:1px solid var(--border);font-weight:600">{_e(a)}</th>'
|
| 119 |
)
|
| 120 |
for b in engines:
|
|
@@ -87,7 +87,7 @@ def build_lexical_modernization_html(
|
|
| 87 |
]
|
| 88 |
for col in (gt_label, hyp_label, n_label, rate_label):
|
| 89 |
parts.append(
|
| 90 |
-
f'<th style="padding:.3rem .5rem;text-align:left;'
|
| 91 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 92 |
f'{_e(col)}</th>'
|
| 93 |
)
|
|
|
|
| 87 |
]
|
| 88 |
for col in (gt_label, hyp_label, n_label, rate_label):
|
| 89 |
parts.append(
|
| 90 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 91 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 92 |
f'{_e(col)}</th>'
|
| 93 |
)
|
|
@@ -112,7 +112,7 @@ def build_longitudinal_html(
|
|
| 112 |
for col in (h_engine, h_n_runs, h_first, h_last, h_delta,
|
| 113 |
h_slope, h_r2, h_change):
|
| 114 |
parts.append(
|
| 115 |
-
f'<th style="padding:.4rem .6rem;text-align:left;'
|
| 116 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 117 |
f'{_e(col)}</th>'
|
| 118 |
)
|
|
|
|
| 112 |
for col in (h_engine, h_n_runs, h_first, h_last, h_delta,
|
| 113 |
h_slope, h_r2, h_change):
|
| 114 |
parts.append(
|
| 115 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 116 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 117 |
f'{_e(col)}</th>'
|
| 118 |
)
|
|
@@ -113,7 +113,7 @@ def build_module_audit_html(
|
|
| 113 |
for col in (h_module, h_status, h_version, h_author,
|
| 114 |
h_license, h_io, h_citation, h_homepage):
|
| 115 |
parts.append(
|
| 116 |
-
f'<th style="padding:.4rem .6rem;text-align:left;'
|
| 117 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 118 |
f'{_e(col)}</th>'
|
| 119 |
)
|
|
|
|
| 113 |
for col in (h_module, h_status, h_version, h_author,
|
| 114 |
h_license, h_io, h_citation, h_homepage):
|
| 115 |
parts.append(
|
| 116 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 117 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 118 |
f'{_e(col)}</th>'
|
| 119 |
)
|
|
@@ -108,7 +108,7 @@ def build_multirun_stability_html(
|
|
| 108 |
]
|
| 109 |
for col in (h_engine, h_n_runs, h_cer, h_cv, h_identical, h_distinct):
|
| 110 |
parts.append(
|
| 111 |
-
f'<th style="padding:.4rem .6rem;text-align:left;'
|
| 112 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 113 |
f'{_e(col)}</th>'
|
| 114 |
)
|
|
|
|
| 108 |
]
|
| 109 |
for col in (h_engine, h_n_runs, h_cer, h_cv, h_identical, h_distinct):
|
| 110 |
parts.append(
|
| 111 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 112 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 113 |
f'{_e(col)}</th>'
|
| 114 |
)
|
|
@@ -96,7 +96,7 @@ def build_ner_summary_html(
|
|
| 96 |
for hdr in (engine_label, f1_label, p_label, r_label,
|
| 97 |
docs_label, halluc_label, missed_label):
|
| 98 |
parts.append(
|
| 99 |
-
f'<th style="padding:.3rem .5rem;text-align:left;'
|
| 100 |
f'border-bottom:1px solid var(--border);font-weight:600">'
|
| 101 |
f'{_e(hdr)}</th>'
|
| 102 |
)
|
|
@@ -194,12 +194,12 @@ def build_ner_per_category_html(
|
|
| 194 |
)
|
| 195 |
parts.append("<thead><tr>")
|
| 196 |
parts.append(
|
| 197 |
-
f'<th style="padding:.3rem .5rem;text-align:left;'
|
| 198 |
f'border-bottom:1px solid var(--border)">{_e(engine_label)}</th>'
|
| 199 |
)
|
| 200 |
for cat in categories:
|
| 201 |
parts.append(
|
| 202 |
-
f'<th style="padding:.3rem .5rem;text-align:center;'
|
| 203 |
f'border-bottom:1px solid var(--border)">{_e(cat)}</th>'
|
| 204 |
)
|
| 205 |
parts.append("</tr></thead><tbody>")
|
|
@@ -207,7 +207,7 @@ def build_ner_per_category_html(
|
|
| 207 |
per_cat = (engine["aggregated_ner"] or {}).get("per_category") or {}
|
| 208 |
parts.append("<tr>")
|
| 209 |
parts.append(
|
| 210 |
-
f'<th style="padding:.3rem .5rem;text-align:right;'
|
| 211 |
f'border-right:1px solid var(--border);font-weight:600">'
|
| 212 |
f'{_e(engine.get("name", ""))}</th>'
|
| 213 |
)
|
|
|
|
| 96 |
for hdr in (engine_label, f1_label, p_label, r_label,
|
| 97 |
docs_label, halluc_label, missed_label):
|
| 98 |
parts.append(
|
| 99 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 100 |
f'border-bottom:1px solid var(--border);font-weight:600">'
|
| 101 |
f'{_e(hdr)}</th>'
|
| 102 |
)
|
|
|
|
| 194 |
)
|
| 195 |
parts.append("<thead><tr>")
|
| 196 |
parts.append(
|
| 197 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 198 |
f'border-bottom:1px solid var(--border)">{_e(engine_label)}</th>'
|
| 199 |
)
|
| 200 |
for cat in categories:
|
| 201 |
parts.append(
|
| 202 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:center;'
|
| 203 |
f'border-bottom:1px solid var(--border)">{_e(cat)}</th>'
|
| 204 |
)
|
| 205 |
parts.append("</tr></thead><tbody>")
|
|
|
|
| 207 |
per_cat = (engine["aggregated_ner"] or {}).get("per_category") or {}
|
| 208 |
parts.append("<tr>")
|
| 209 |
parts.append(
|
| 210 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:right;'
|
| 211 |
f'border-right:1px solid var(--border);font-weight:600">'
|
| 212 |
f'{_e(engine.get("name", ""))}</th>'
|
| 213 |
)
|
|
@@ -103,16 +103,16 @@ def build_numerical_sequences_html(
|
|
| 103 |
'<table style="border-collapse:collapse;width:100%;'
|
| 104 |
'font-size:.9rem">',
|
| 105 |
'<thead><tr>',
|
| 106 |
-
f'<th style="padding:.4rem .6rem;text-align:left;'
|
| 107 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 108 |
f'{_e(col_engine)}</th>',
|
| 109 |
-
f'<th style="padding:.4rem .6rem;text-align:right;'
|
| 110 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 111 |
f'{_e(col_global)}</th>',
|
| 112 |
]
|
| 113 |
for cat in visible_cats:
|
| 114 |
parts.append(
|
| 115 |
-
f'<th style="padding:.4rem .6rem;text-align:right;'
|
| 116 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 117 |
f'{_e(cat_label.get(cat, cat))}</th>'
|
| 118 |
)
|
|
|
|
| 103 |
'<table style="border-collapse:collapse;width:100%;'
|
| 104 |
'font-size:.9rem">',
|
| 105 |
'<thead><tr>',
|
| 106 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 107 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 108 |
f'{_e(col_engine)}</th>',
|
| 109 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 110 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 111 |
f'{_e(col_global)}</th>',
|
| 112 |
]
|
| 113 |
for cat in visible_cats:
|
| 114 |
parts.append(
|
| 115 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 116 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 117 |
f'{_e(cat_label.get(cat, cat))}</th>'
|
| 118 |
)
|
|
@@ -99,13 +99,13 @@ def _table_header(
|
|
| 99 |
"""Construit l'entête d'un tableau moteur × colonnes."""
|
| 100 |
parts = [
|
| 101 |
'<thead><tr>',
|
| 102 |
-
f'<th style="padding:.3rem .5rem;text-align:left;'
|
| 103 |
f'border-bottom:1px solid var(--border);font-weight:600">'
|
| 104 |
f'{_e(engine_label)}</th>',
|
| 105 |
]
|
| 106 |
for col in columns:
|
| 107 |
parts.append(
|
| 108 |
-
f'<th style="padding:.3rem .5rem;text-align:center;'
|
| 109 |
f'border-bottom:1px solid var(--border);font-weight:600">'
|
| 110 |
f'{_e(col)}</th>'
|
| 111 |
)
|
|
@@ -424,30 +424,30 @@ def build_modern_archives_section(
|
|
| 424 |
)
|
| 425 |
parts.append("<thead><tr>")
|
| 426 |
parts.append(
|
| 427 |
-
f'<th rowspan="2" style="padding:.3rem .5rem;text-align:left;'
|
| 428 |
f'border-bottom:1px solid var(--border);font-weight:600">'
|
| 429 |
f'{_e(engine_label)}</th>'
|
| 430 |
)
|
| 431 |
parts.append(
|
| 432 |
-
f'<th colspan="2" style="padding:.3rem .5rem;text-align:center;'
|
| 433 |
f'border-bottom:1px solid var(--border);font-weight:600">'
|
| 434 |
f'{_e(global_label)}</th>'
|
| 435 |
)
|
| 436 |
for cat in cats:
|
| 437 |
parts.append(
|
| 438 |
-
f'<th colspan="2" style="padding:.3rem .5rem;text-align:center;'
|
| 439 |
f'border-bottom:1px solid var(--border);font-weight:600">'
|
| 440 |
f'{_e(cat)}</th>'
|
| 441 |
)
|
| 442 |
parts.append("</tr><tr>")
|
| 443 |
for _ in range(1 + len(cats)):
|
| 444 |
parts.append(
|
| 445 |
-
f'<th style="padding:.2rem .4rem;text-align:center;'
|
| 446 |
f'font-size:.75rem;font-weight:500;opacity:.7">'
|
| 447 |
f'{_e(strict_label)}</th>'
|
| 448 |
)
|
| 449 |
parts.append(
|
| 450 |
-
f'<th style="padding:.2rem .4rem;text-align:center;'
|
| 451 |
f'font-size:.75rem;font-weight:500;opacity:.7">'
|
| 452 |
f'{_e(expansion_label)}</th>'
|
| 453 |
)
|
|
|
|
| 99 |
"""Construit l'entête d'un tableau moteur × colonnes."""
|
| 100 |
parts = [
|
| 101 |
'<thead><tr>',
|
| 102 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 103 |
f'border-bottom:1px solid var(--border);font-weight:600">'
|
| 104 |
f'{_e(engine_label)}</th>',
|
| 105 |
]
|
| 106 |
for col in columns:
|
| 107 |
parts.append(
|
| 108 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:center;'
|
| 109 |
f'border-bottom:1px solid var(--border);font-weight:600">'
|
| 110 |
f'{_e(col)}</th>'
|
| 111 |
)
|
|
|
|
| 424 |
)
|
| 425 |
parts.append("<thead><tr>")
|
| 426 |
parts.append(
|
| 427 |
+
f'<th scope=\"col\" rowspan="2" style="padding:.3rem .5rem;text-align:left;'
|
| 428 |
f'border-bottom:1px solid var(--border);font-weight:600">'
|
| 429 |
f'{_e(engine_label)}</th>'
|
| 430 |
)
|
| 431 |
parts.append(
|
| 432 |
+
f'<th scope=\"col\" colspan="2" style="padding:.3rem .5rem;text-align:center;'
|
| 433 |
f'border-bottom:1px solid var(--border);font-weight:600">'
|
| 434 |
f'{_e(global_label)}</th>'
|
| 435 |
)
|
| 436 |
for cat in cats:
|
| 437 |
parts.append(
|
| 438 |
+
f'<th scope=\"col\" colspan="2" style="padding:.3rem .5rem;text-align:center;'
|
| 439 |
f'border-bottom:1px solid var(--border);font-weight:600">'
|
| 440 |
f'{_e(cat)}</th>'
|
| 441 |
)
|
| 442 |
parts.append("</tr><tr>")
|
| 443 |
for _ in range(1 + len(cats)):
|
| 444 |
parts.append(
|
| 445 |
+
f'<th scope=\"col\" style="padding:.2rem .4rem;text-align:center;'
|
| 446 |
f'font-size:.75rem;font-weight:500;opacity:.7">'
|
| 447 |
f'{_e(strict_label)}</th>'
|
| 448 |
)
|
| 449 |
parts.append(
|
| 450 |
+
f'<th scope=\"col\" style="padding:.2rem .4rem;text-align:center;'
|
| 451 |
f'font-size:.75rem;font-weight:500;opacity:.7">'
|
| 452 |
f'{_e(expansion_label)}</th>'
|
| 453 |
)
|
|
@@ -187,7 +187,7 @@ def build_pipeline_steps_table_html(
|
|
| 187 |
dmean_label, dmedian_label, metrics_label, errors_label,
|
| 188 |
):
|
| 189 |
parts.append(
|
| 190 |
-
f'<th style="padding:.3rem .5rem;text-align:left;'
|
| 191 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 192 |
f'{_e(col)}</th>'
|
| 193 |
)
|
|
@@ -428,7 +428,7 @@ def build_pipeline_ranking_table_html(
|
|
| 428 |
]
|
| 429 |
for col in (rank_label, name_label, value_label):
|
| 430 |
parts.append(
|
| 431 |
-
f'<th style="padding:.3rem .5rem;text-align:left;'
|
| 432 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 433 |
f'{_e(col)}</th>'
|
| 434 |
)
|
|
@@ -504,7 +504,7 @@ def build_pipeline_gain_table_html(
|
|
| 504 |
]
|
| 505 |
for col in (name_label, value_label, abs_label, rel_label):
|
| 506 |
parts.append(
|
| 507 |
-
f'<th style="padding:.3rem .5rem;text-align:left;'
|
| 508 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 509 |
f'{_e(col)}</th>'
|
| 510 |
)
|
|
|
|
| 187 |
dmean_label, dmedian_label, metrics_label, errors_label,
|
| 188 |
):
|
| 189 |
parts.append(
|
| 190 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 191 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 192 |
f'{_e(col)}</th>'
|
| 193 |
)
|
|
|
|
| 428 |
]
|
| 429 |
for col in (rank_label, name_label, value_label):
|
| 430 |
parts.append(
|
| 431 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 432 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 433 |
f'{_e(col)}</th>'
|
| 434 |
)
|
|
|
|
| 504 |
]
|
| 505 |
for col in (name_label, value_label, abs_label, rel_label):
|
| 506 |
parts.append(
|
| 507 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 508 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 509 |
f'{_e(col)}</th>'
|
| 510 |
)
|
|
@@ -94,7 +94,7 @@ def build_readability_summary_html(
|
|
| 94 |
for col in (col_engine, col_mean, col_median, col_over,
|
| 95 |
col_under, col_docs):
|
| 96 |
parts.append(
|
| 97 |
-
f'<th style="padding:.4rem .6rem;text-align:left;'
|
| 98 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 99 |
f'{_e(col)}</th>'
|
| 100 |
)
|
|
|
|
| 94 |
for col in (col_engine, col_mean, col_median, col_over,
|
| 95 |
col_under, col_docs):
|
| 96 |
parts.append(
|
| 97 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 98 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 99 |
f'{_e(col)}</th>'
|
| 100 |
)
|
|
@@ -89,7 +89,7 @@ def _build_summary_table(
|
|
| 89 |
]
|
| 90 |
for col in (h_engine, h_total, h_n_types, h_worst):
|
| 91 |
parts.append(
|
| 92 |
-
f'<th style="padding:.4rem .6rem;text-align:left;'
|
| 93 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 94 |
f'{_e(col)}</th>'
|
| 95 |
)
|
|
@@ -147,7 +147,7 @@ def _build_detail_table(
|
|
| 147 |
for col in (h_engine, h_deg_type, h_n_docs,
|
| 148 |
h_n_with_data, h_deficit, h_above):
|
| 149 |
parts.append(
|
| 150 |
-
f'<th style="padding:.4rem .6rem;text-align:left;'
|
| 151 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 152 |
f'{_e(col)}</th>'
|
| 153 |
)
|
|
|
|
| 89 |
]
|
| 90 |
for col in (h_engine, h_total, h_n_types, h_worst):
|
| 91 |
parts.append(
|
| 92 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 93 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 94 |
f'{_e(col)}</th>'
|
| 95 |
)
|
|
|
|
| 147 |
for col in (h_engine, h_deg_type, h_n_docs,
|
| 148 |
h_n_with_data, h_deficit, h_above):
|
| 149 |
parts.append(
|
| 150 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 151 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 152 |
f'{_e(col)}</th>'
|
| 153 |
)
|
|
@@ -87,7 +87,7 @@ def build_searchability_summary_html(
|
|
| 87 |
]
|
| 88 |
for col in (col_engine, col_recall, col_count, col_docs):
|
| 89 |
parts.append(
|
| 90 |
-
f'<th style="padding:.4rem .6rem;text-align:left;'
|
| 91 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 92 |
f'{_e(col)}</th>'
|
| 93 |
)
|
|
|
|
| 87 |
]
|
| 88 |
for col in (col_engine, col_recall, col_count, col_docs):
|
| 89 |
parts.append(
|
| 90 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 91 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 92 |
f'{_e(col)}</th>'
|
| 93 |
)
|
|
@@ -89,7 +89,7 @@ def build_specialization_html(
|
|
| 89 |
]
|
| 90 |
for col in (h_a, h_b, h_score, h_cat):
|
| 91 |
parts.append(
|
| 92 |
-
f'<th style="padding:.4rem .6rem;text-align:left;'
|
| 93 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 94 |
f'{_e(col)}</th>'
|
| 95 |
)
|
|
|
|
| 89 |
]
|
| 90 |
for col in (h_a, h_b, h_score, h_cat):
|
| 91 |
parts.append(
|
| 92 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 93 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 94 |
f'{_e(col)}</th>'
|
| 95 |
)
|
|
@@ -157,7 +157,7 @@ def build_stratified_ranking_html(
|
|
| 157 |
parts.append("<thead><tr>")
|
| 158 |
for hdr in (engine_label, median_label, mean_label, docs_label):
|
| 159 |
parts.append(
|
| 160 |
-
f'<th style="padding:.3rem .5rem;text-align:left;'
|
| 161 |
f'border-bottom:1px solid var(--border);font-weight:600">'
|
| 162 |
f'{_e(hdr)}</th>'
|
| 163 |
)
|
|
|
|
| 157 |
parts.append("<thead><tr>")
|
| 158 |
for hdr in (engine_label, median_label, mean_label, docs_label):
|
| 159 |
parts.append(
|
| 160 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 161 |
f'border-bottom:1px solid var(--border);font-weight:600">'
|
| 162 |
f'{_e(hdr)}</th>'
|
| 163 |
)
|
|
@@ -158,13 +158,13 @@ def _build_recoverability_summary_html(
|
|
| 158 |
'<table style="border-collapse:collapse;font-size:.85rem;'
|
| 159 |
'margin-top:.5rem">',
|
| 160 |
'<thead><tr>',
|
| 161 |
-
'<th style="padding:.2rem .5rem;text-align:left;'
|
| 162 |
'border-bottom:1px solid #ccc">'
|
| 163 |
f'{_e(labels.get("taxocomp_level_label", "Catégorie"))}</th>',
|
| 164 |
-
'<th style="padding:.2rem .5rem;text-align:right;'
|
| 165 |
'border-bottom:1px solid #ccc">'
|
| 166 |
f'{_e(_e(data["engine_a"]))}</th>',
|
| 167 |
-
'<th style="padding:.2rem .5rem;text-align:right;'
|
| 168 |
'border-bottom:1px solid #ccc">'
|
| 169 |
f'{_e(_e(data["engine_b"]))}</th>',
|
| 170 |
'</tr></thead><tbody>',
|
|
|
|
| 158 |
'<table style="border-collapse:collapse;font-size:.85rem;'
|
| 159 |
'margin-top:.5rem">',
|
| 160 |
'<thead><tr>',
|
| 161 |
+
'<th scope=\"col\" style="padding:.2rem .5rem;text-align:left;'
|
| 162 |
'border-bottom:1px solid #ccc">'
|
| 163 |
f'{_e(labels.get("taxocomp_level_label", "Catégorie"))}</th>',
|
| 164 |
+
'<th scope=\"col\" style="padding:.2rem .5rem;text-align:right;'
|
| 165 |
'border-bottom:1px solid #ccc">'
|
| 166 |
f'{_e(_e(data["engine_a"]))}</th>',
|
| 167 |
+
'<th scope=\"col\" style="padding:.2rem .5rem;text-align:right;'
|
| 168 |
'border-bottom:1px solid #ccc">'
|
| 169 |
f'{_e(_e(data["engine_b"]))}</th>',
|
| 170 |
'</tr></thead><tbody>',
|
|
@@ -122,10 +122,10 @@ def _build_top_pairs_table(
|
|
| 122 |
'<table style="border-collapse:collapse;font-size:.85rem;'
|
| 123 |
'margin-top:.5rem">',
|
| 124 |
'<thead><tr>',
|
| 125 |
-
f'<th style="padding:.3rem .5rem;text-align:left;'
|
| 126 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 127 |
f'{_e(pair_label)}</th>',
|
| 128 |
-
f'<th style="padding:.3rem .5rem;text-align:right;'
|
| 129 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 130 |
f'{_e(jaccard_label)}</th>',
|
| 131 |
'</tr></thead><tbody>',
|
|
|
|
| 122 |
'<table style="border-collapse:collapse;font-size:.85rem;'
|
| 123 |
'margin-top:.5rem">',
|
| 124 |
'<thead><tr>',
|
| 125 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 126 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 127 |
f'{_e(pair_label)}</th>',
|
| 128 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:right;'
|
| 129 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 130 |
f'{_e(jaccard_label)}</th>',
|
| 131 |
'</tr></thead><tbody>',
|
|
@@ -26,6 +26,11 @@ function _switchView(name) {
|
|
| 26 |
function showView(name) {
|
| 27 |
_switchView(name);
|
| 28 |
updateURL(name);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
}
|
| 30 |
|
| 31 |
// ── Formatage ───────────────────────────────────────────────────
|
|
@@ -2600,4 +2605,117 @@ function init() {
|
|
| 2600 |
});
|
| 2601 |
}
|
| 2602 |
|
| 2603 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
function showView(name) {
|
| 27 |
_switchView(name);
|
| 28 |
updateURL(name);
|
| 29 |
+
// Sprint A6 — re-attache les boutons d'a11y aux nouveaux charts
|
| 30 |
+
// qui ont été instanciés paresseusement au switch de vue.
|
| 31 |
+
if (typeof attachChartA11y === 'function') {
|
| 32 |
+
setTimeout(attachChartA11y, 50);
|
| 33 |
+
}
|
| 34 |
}
|
| 35 |
|
| 36 |
// ── Formatage ───────────────────────────────────────────────────
|
|
|
|
| 2605 |
});
|
| 2606 |
}
|
| 2607 |
|
| 2608 |
+
// ─── Sprint A6 (B-9) — accessibilité des graphiques Chart.js ──────────
|
| 2609 |
+
//
|
| 2610 |
+
// Les <canvas> Chart.js ne sont **pas** accessibles aux lecteurs d'écran
|
| 2611 |
+
// par défaut (le rendu est purement pixel). Pour respecter WCAG 1.1.1
|
| 2612 |
+
// (Non-text Content) niveau A, on ajoute :
|
| 2613 |
+
//
|
| 2614 |
+
// 1. ``role="img"`` + ``aria-label`` (déjà posés statiquement dans le
|
| 2615 |
+
// HTML via le helper Python ``_enrich_canvas_with_aria``) ;
|
| 2616 |
+
// 2. une table de données jumelle générée à la demande à partir de
|
| 2617 |
+
// l'instance Chart.js, avec un bouton "Voir les données" qui la
|
| 2618 |
+
// révèle pour TOUS (utile aussi pour la copie / vérification).
|
| 2619 |
+
//
|
| 2620 |
+
// Cette fonction est idempotente : on peut l'appeler plusieurs fois
|
| 2621 |
+
// sans dupliquer les boutons (test ``data-a11y-attached``).
|
| 2622 |
+
function attachChartA11y() {
|
| 2623 |
+
const canvases = document.querySelectorAll('canvas[data-a11y-label]');
|
| 2624 |
+
canvases.forEach(canvas => {
|
| 2625 |
+
if (canvas.dataset.a11yAttached === '1') return;
|
| 2626 |
+
canvas.dataset.a11yAttached = '1';
|
| 2627 |
+
|
| 2628 |
+
const id = canvas.id;
|
| 2629 |
+
if (!id) return;
|
| 2630 |
+
|
| 2631 |
+
// Bouton "Voir les données" en dessous du canvas.
|
| 2632 |
+
const btn = document.createElement('button');
|
| 2633 |
+
btn.type = 'button';
|
| 2634 |
+
btn.className = 'btn-toggle-data';
|
| 2635 |
+
btn.setAttribute('data-i18n', 'view_data');
|
| 2636 |
+
btn.textContent = (typeof I18N !== 'undefined' && I18N.view_data)
|
| 2637 |
+
? I18N.view_data : 'Voir les données';
|
| 2638 |
+
btn.setAttribute('aria-controls', id + '-data');
|
| 2639 |
+
btn.setAttribute('aria-expanded', 'false');
|
| 2640 |
+
|
| 2641 |
+
// Conteneur de table (caché visuellement mais lu par les AT via
|
| 2642 |
+
// aria-describedby ; révélé visuellement au clic via .is-revealed).
|
| 2643 |
+
const wrapper = document.createElement('div');
|
| 2644 |
+
wrapper.id = id + '-data';
|
| 2645 |
+
wrapper.className = 'chart-data-table visually-hidden';
|
| 2646 |
+
wrapper.setAttribute('role', 'region');
|
| 2647 |
+
wrapper.setAttribute('aria-label',
|
| 2648 |
+
((typeof I18N !== 'undefined' && I18N.chart_data_caption)
|
| 2649 |
+
? I18N.chart_data_caption
|
| 2650 |
+
: 'Données du graphique')
|
| 2651 |
+
+ ' : ' + (canvas.dataset.a11yLabel || id));
|
| 2652 |
+
|
| 2653 |
+
// Lien aria-describedby pour que le lecteur d'écran annonce
|
| 2654 |
+
// l'existence de la table dès qu'il atteint le canvas.
|
| 2655 |
+
canvas.setAttribute('aria-describedby', wrapper.id);
|
| 2656 |
+
|
| 2657 |
+
btn.addEventListener('click', () => {
|
| 2658 |
+
const expanded = wrapper.classList.toggle('is-revealed');
|
| 2659 |
+
btn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
| 2660 |
+
btn.textContent = expanded
|
| 2661 |
+
? ((typeof I18N !== 'undefined' && I18N.hide_data) ? I18N.hide_data : 'Masquer les données')
|
| 2662 |
+
: ((typeof I18N !== 'undefined' && I18N.view_data) ? I18N.view_data : 'Voir les données');
|
| 2663 |
+
// Génération paresseuse du tableau au premier clic.
|
| 2664 |
+
if (expanded && !wrapper.dataset.populated) {
|
| 2665 |
+
_populateChartDataTable(wrapper, id);
|
| 2666 |
+
wrapper.dataset.populated = '1';
|
| 2667 |
+
}
|
| 2668 |
+
});
|
| 2669 |
+
|
| 2670 |
+
canvas.parentElement.appendChild(btn);
|
| 2671 |
+
canvas.parentElement.appendChild(wrapper);
|
| 2672 |
+
});
|
| 2673 |
+
}
|
| 2674 |
+
|
| 2675 |
+
function _populateChartDataTable(wrapper, canvasId) {
|
| 2676 |
+
const chart = (typeof chartInstances !== 'undefined') ? chartInstances[canvasId] : null;
|
| 2677 |
+
if (!chart || !chart.data) {
|
| 2678 |
+
wrapper.innerHTML = '<p>' +
|
| 2679 |
+
((typeof I18N !== 'undefined' && I18N.chart_no_data)
|
| 2680 |
+
? I18N.chart_no_data : 'Aucune donnée disponible')
|
| 2681 |
+
+ '</p>';
|
| 2682 |
+
return;
|
| 2683 |
+
}
|
| 2684 |
+
const labels = chart.data.labels || [];
|
| 2685 |
+
const datasets = chart.data.datasets || [];
|
| 2686 |
+
|
| 2687 |
+
// En-tête : colonne libellé puis une colonne par dataset.
|
| 2688 |
+
let html = '<table class="chart-data-table is-revealed">';
|
| 2689 |
+
html += '<thead><tr><th scope="col">—</th>';
|
| 2690 |
+
datasets.forEach(ds => {
|
| 2691 |
+
html += '<th scope="col">' + esc(ds.label || '') + '</th>';
|
| 2692 |
+
});
|
| 2693 |
+
html += '</tr></thead><tbody>';
|
| 2694 |
+
// Une ligne par label.
|
| 2695 |
+
for (let i = 0; i < labels.length; i++) {
|
| 2696 |
+
html += '<tr><th scope="row">' + esc(String(labels[i])) + '</th>';
|
| 2697 |
+
datasets.forEach(ds => {
|
| 2698 |
+
const v = ds.data ? ds.data[i] : '';
|
| 2699 |
+
html += '<td>' + esc(String(v == null ? '' : v)) + '</td>';
|
| 2700 |
+
});
|
| 2701 |
+
html += '</tr>';
|
| 2702 |
+
}
|
| 2703 |
+
// Cas particulier : pas de labels (scatter, radar) — on dump les datasets.
|
| 2704 |
+
if (labels.length === 0 && datasets.length > 0) {
|
| 2705 |
+
datasets.forEach(ds => {
|
| 2706 |
+
html += '<tr><th scope="row">' + esc(ds.label || '') + '</th><td>' +
|
| 2707 |
+
esc(JSON.stringify(ds.data).slice(0, 200)) + '</td></tr>';
|
| 2708 |
+
});
|
| 2709 |
+
}
|
| 2710 |
+
html += '</tbody></table>';
|
| 2711 |
+
wrapper.innerHTML = html;
|
| 2712 |
+
}
|
| 2713 |
+
|
| 2714 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2715 |
+
init();
|
| 2716 |
+
// Délai pour laisser les charts s'instancier au switch de vue.
|
| 2717 |
+
// Les boutons sont posés sur les canvas déjà visibles ; pour les
|
| 2718 |
+
// canvas qui se créent au premier showView('analyses'), on rappelle
|
| 2719 |
+
// attachChartA11y depuis showView aussi.
|
| 2720 |
+
setTimeout(attachChartA11y, 200);
|
| 2721 |
+
});
|
|
@@ -1,4 +1,10 @@
|
|
| 1 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
<!-- ── Navigation ─────────────────────────────────────────────────── -->
|
| 3 |
<nav>
|
| 4 |
<div class="brand">
|
|
@@ -22,9 +28,10 @@
|
|
| 22 |
<!-- ── Bandeau exclusion globale ───────────────────────────────────── -->
|
| 23 |
<div id="global-exclusion-banner" style="display:none;background:#fef3c7;border-bottom:2px solid #f59e0b;padding:.5rem 1.5rem;font-size:.85rem;font-weight:600;color:#92400e;text-align:center">
|
| 24 |
<span id="global-exclusion-text"></span>
|
| 25 |
-
<button onclick="resetAllExclusions()" style="margin-left:1rem;font-size:.75rem;padding:.15rem .5rem;border:1px solid #d97706;background:#fff;border-radius:.25rem;cursor:pointer">Réinitialiser</button>
|
| 26 |
</div>
|
| 27 |
|
| 28 |
-
<!-- ── Main ────────────
|
| 29 |
-
<main>
|
|
|
|
| 30 |
|
|
|
|
| 1 |
|
| 2 |
+
<!-- ── Skip-to-content (Sprint A6, B-10) ───────────────────────────────
|
| 3 |
+
Lien WCAG 2.4.1 (Bypass Blocks) — premier enfant tabbable du body,
|
| 4 |
+
visible uniquement au focus, saute la nav et le bandeau pour
|
| 5 |
+
l'utilisateur clavier ou lecteur d'écran. -->
|
| 6 |
+
<a class="skip-link" href="#main" data-i18n="skip_to_content">Aller au contenu</a>
|
| 7 |
+
|
| 8 |
<!-- ── Navigation ─────────────────────────────────────────────────── -->
|
| 9 |
<nav>
|
| 10 |
<div class="brand">
|
|
|
|
| 28 |
<!-- ── Bandeau exclusion globale ───────────────────────────────────── -->
|
| 29 |
<div id="global-exclusion-banner" style="display:none;background:#fef3c7;border-bottom:2px solid #f59e0b;padding:.5rem 1.5rem;font-size:.85rem;font-weight:600;color:#92400e;text-align:center">
|
| 30 |
<span id="global-exclusion-text"></span>
|
| 31 |
+
<button onclick="resetAllExclusions()" data-i18n="reset_all" style="margin-left:1rem;font-size:.75rem;padding:.15rem .5rem;border:1px solid #d97706;background:#fff;border-radius:.25rem;cursor:pointer">Réinitialiser</button>
|
| 32 |
</div>
|
| 33 |
|
| 34 |
+
<!-- ── Main (Sprint A6, B-10 : id=main pour le skip-link) ──────────── -->
|
| 35 |
+
<main id="main" role="main">
|
| 36 |
+
|
| 37 |
|
|
@@ -913,3 +913,77 @@ body.present-mode .btn-customize { display: none !important; }
|
|
| 913 |
text-decoration: underline;
|
| 914 |
}
|
| 915 |
body.present-mode .synth-cases-link { display: none; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 913 |
text-decoration: underline;
|
| 914 |
}
|
| 915 |
body.present-mode .synth-cases-link { display: none; }
|
| 916 |
+
|
| 917 |
+
/* ── Sprint A6 — Accessibilité WCAG niveau A ─────────────────────────
|
| 918 |
+
Skip-to-content link (B-10) : caché hors :focus pour ne pas polluer
|
| 919 |
+
le visuel mais immédiatement disponible au clavier (Tab depuis la URL
|
| 920 |
+
bar atteint le contenu en 1 tabulation). Contraste AA ≥ 4.5:1 sur
|
| 921 |
+
fond bleu institutionnel. */
|
| 922 |
+
.skip-link {
|
| 923 |
+
position: absolute;
|
| 924 |
+
top: -40px;
|
| 925 |
+
left: 0;
|
| 926 |
+
background: #1d4ed8;
|
| 927 |
+
color: #ffffff;
|
| 928 |
+
padding: .5rem 1rem;
|
| 929 |
+
font-weight: 600;
|
| 930 |
+
text-decoration: none;
|
| 931 |
+
z-index: 9999;
|
| 932 |
+
border-radius: 0 0 .25rem 0;
|
| 933 |
+
transition: top 120ms ease-out;
|
| 934 |
+
}
|
| 935 |
+
.skip-link:focus {
|
| 936 |
+
top: 0;
|
| 937 |
+
outline: 3px solid #fbbf24;
|
| 938 |
+
outline-offset: 2px;
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
/* Sprint A6 (B-9) — table jumelle des charts pour lecteurs d'écran.
|
| 942 |
+
``visually-hidden`` cache le contenu visuellement tout en le
|
| 943 |
+
conservant accessible aux AT (NVDA, JAWS, VoiceOver). Le bouton
|
| 944 |
+
``Voir les données`` peut révéler la table à tous via toggle de
|
| 945 |
+
classe. */
|
| 946 |
+
.visually-hidden {
|
| 947 |
+
position: absolute;
|
| 948 |
+
width: 1px;
|
| 949 |
+
height: 1px;
|
| 950 |
+
padding: 0;
|
| 951 |
+
margin: -1px;
|
| 952 |
+
overflow: hidden;
|
| 953 |
+
clip: rect(0, 0, 0, 0);
|
| 954 |
+
white-space: nowrap;
|
| 955 |
+
border: 0;
|
| 956 |
+
}
|
| 957 |
+
.chart-data-table {
|
| 958 |
+
margin-top: .5rem;
|
| 959 |
+
font-size: .85rem;
|
| 960 |
+
border-collapse: collapse;
|
| 961 |
+
}
|
| 962 |
+
.chart-data-table th, .chart-data-table td {
|
| 963 |
+
border: 1px solid var(--border-light, #e2e8f0);
|
| 964 |
+
padding: .25rem .5rem;
|
| 965 |
+
text-align: left;
|
| 966 |
+
}
|
| 967 |
+
.chart-data-table.is-revealed {
|
| 968 |
+
position: static;
|
| 969 |
+
width: auto;
|
| 970 |
+
height: auto;
|
| 971 |
+
clip: auto;
|
| 972 |
+
white-space: normal;
|
| 973 |
+
}
|
| 974 |
+
.btn-toggle-data {
|
| 975 |
+
margin-top: .5rem;
|
| 976 |
+
font-size: .78rem;
|
| 977 |
+
padding: .15rem .5rem;
|
| 978 |
+
border: 1px solid var(--border-light, #cbd5e1);
|
| 979 |
+
background: #ffffff;
|
| 980 |
+
border-radius: .25rem;
|
| 981 |
+
cursor: pointer;
|
| 982 |
+
}
|
| 983 |
+
.btn-toggle-data:hover {
|
| 984 |
+
background: #f1f5f9;
|
| 985 |
+
}
|
| 986 |
+
.btn-toggle-data:focus {
|
| 987 |
+
outline: 2px solid #1d4ed8;
|
| 988 |
+
outline-offset: 2px;
|
| 989 |
+
}
|
|
@@ -6,14 +6,14 @@
|
|
| 6 |
<div class="chart-card">
|
| 7 |
<h3 data-i18n="h_cer_dist">Distribution du CER par moteur</h3>
|
| 8 |
<div class="chart-canvas-wrap">
|
| 9 |
-
<canvas id="chart-cer-hist"></canvas>
|
| 10 |
</div>
|
| 11 |
</div>
|
| 12 |
|
| 13 |
<div class="chart-card">
|
| 14 |
<h3 data-i18n="h_radar">Profil des moteurs (radar)</h3>
|
| 15 |
<div class="chart-canvas-wrap">
|
| 16 |
-
<canvas id="chart-radar"></canvas>
|
| 17 |
</div>
|
| 18 |
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.5rem" data-i18n="radar_note">
|
| 19 |
Axe radar : CER, WER, MER, WIL — valeurs inversées (plus c'est haut, meilleur est le moteur).
|
|
@@ -23,21 +23,21 @@
|
|
| 23 |
<div class="chart-card">
|
| 24 |
<h3 data-i18n="h_cer_doc">CER par document (tous moteurs)</h3>
|
| 25 |
<div class="chart-canvas-wrap">
|
| 26 |
-
<canvas id="chart-cer-doc"></canvas>
|
| 27 |
</div>
|
| 28 |
</div>
|
| 29 |
|
| 30 |
<div class="chart-card">
|
| 31 |
<h3 data-i18n="h_duration">Temps d'exécution moyen (secondes/document)</h3>
|
| 32 |
<div class="chart-canvas-wrap">
|
| 33 |
-
<canvas id="chart-duration"></canvas>
|
| 34 |
</div>
|
| 35 |
</div>
|
| 36 |
|
| 37 |
<div class="chart-card">
|
| 38 |
<h3 data-i18n="h_quality_cer">Qualité image ↔ CER (scatter plot)</h3>
|
| 39 |
<div class="chart-canvas-wrap">
|
| 40 |
-
<canvas id="chart-quality-cer"></canvas>
|
| 41 |
</div>
|
| 42 |
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="quality_cer_note">
|
| 43 |
Chaque point = un document. Axe X = score qualité image [0–1]. Axe Y = CER. Corrélation négative attendue.
|
|
@@ -47,7 +47,7 @@
|
|
| 47 |
<div class="chart-card" style="grid-column:1/-1">
|
| 48 |
<h3 data-i18n="h_taxonomy">Taxonomie des erreurs par moteur</h3>
|
| 49 |
<div class="chart-canvas-wrap" style="max-height:300px">
|
| 50 |
-
<canvas id="chart-taxonomy"></canvas>
|
| 51 |
</div>
|
| 52 |
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="taxonomy_note">
|
| 53 |
Distribution des classes d'erreurs (classes 1–9 de la taxonomie Picarones).
|
|
@@ -58,7 +58,7 @@
|
|
| 58 |
<div class="chart-card" style="grid-column:1/-1">
|
| 59 |
<h3 data-i18n="h_reliability">Courbes de fiabilité</h3>
|
| 60 |
<div class="chart-canvas-wrap" style="max-height:300px">
|
| 61 |
-
<canvas id="chart-reliability"></canvas>
|
| 62 |
</div>
|
| 63 |
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="reliability_note">
|
| 64 |
Pour les X% documents les plus faciles (triés par CER croissant), quel est le CER moyen cumulé ?
|
|
@@ -70,7 +70,7 @@
|
|
| 70 |
<div class="chart-card">
|
| 71 |
<h3 data-i18n="h_bootstrap">Intervalles de confiance à 95 % (bootstrap)</h3>
|
| 72 |
<div class="chart-canvas-wrap">
|
| 73 |
-
<canvas id="chart-bootstrap-ci"></canvas>
|
| 74 |
</div>
|
| 75 |
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="bootstrap_note">
|
| 76 |
IC à 95% sur le CER moyen par moteur (1000 itérations bootstrap).
|
|
@@ -106,7 +106,7 @@
|
|
| 106 |
<div class="chart-card">
|
| 107 |
<h3 data-i18n="h_gini_cer">Gini vs CER moyen <span style="font-size:.72rem;font-weight:400;color:var(--text-muted)" data-i18n="gini_cer_ideal">— idéal : bas-gauche</span></h3>
|
| 108 |
<div class="chart-canvas-wrap">
|
| 109 |
-
<canvas id="chart-gini-cer"></canvas>
|
| 110 |
</div>
|
| 111 |
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="gini_cer_note">
|
| 112 |
Axe X = CER moyen, Axe Y = coefficient de Gini. Un moteur idéal a CER bas ET Gini bas (erreurs rares et uniformes).
|
|
@@ -117,7 +117,7 @@
|
|
| 117 |
<div class="chart-card">
|
| 118 |
<h3 data-i18n="h_ratio_anchor">Ratio longueur vs ancrage <span style="font-size:.72rem;font-weight:400;color:var(--text-muted)" data-i18n="ratio_anchor_subtitle">— hallucinations VLM</span></h3>
|
| 119 |
<div class="chart-canvas-wrap">
|
| 120 |
-
<canvas id="chart-ratio-anchor"></canvas>
|
| 121 |
</div>
|
| 122 |
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="ratio_anchor_note">
|
| 123 |
Axe X = score d'ancrage trigrammes [0–1]. Axe Y = ratio longueur sortie/GT.
|
|
@@ -137,7 +137,7 @@
|
|
| 137 |
onclick="setParetoAxis('co2')" data-i18n="pareto_axis_co2"
|
| 138 |
title="Estimation expérimentale">Carbone (g CO₂)</button>
|
| 139 |
</div>
|
| 140 |
-
<div class="chart-canvas-wrap"><canvas id="pareto-chart"></canvas></div>
|
| 141 |
<div id="pareto-method-note" class="pareto-note" data-i18n="pareto_note">
|
| 142 |
Les moteurs sur la frontière de Pareto (en évidence) sont ceux pour
|
| 143 |
lesquels aucun autre moteur n'offre simultanément un meilleur CER ET
|
|
|
|
| 6 |
<div class="chart-card">
|
| 7 |
<h3 data-i18n="h_cer_dist">Distribution du CER par moteur</h3>
|
| 8 |
<div class="chart-canvas-wrap">
|
| 9 |
+
<canvas id="chart-cer-hist" role="img" aria-label="Distribution des CER par moteur" data-a11y-label="Distribution des CER par moteur"></canvas>
|
| 10 |
</div>
|
| 11 |
</div>
|
| 12 |
|
| 13 |
<div class="chart-card">
|
| 14 |
<h3 data-i18n="h_radar">Profil des moteurs (radar)</h3>
|
| 15 |
<div class="chart-canvas-wrap">
|
| 16 |
+
<canvas id="chart-radar" role="img" aria-label="Profil radar par moteur" data-a11y-label="Profil radar par moteur"></canvas>
|
| 17 |
</div>
|
| 18 |
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.5rem" data-i18n="radar_note">
|
| 19 |
Axe radar : CER, WER, MER, WIL — valeurs inversées (plus c'est haut, meilleur est le moteur).
|
|
|
|
| 23 |
<div class="chart-card">
|
| 24 |
<h3 data-i18n="h_cer_doc">CER par document (tous moteurs)</h3>
|
| 25 |
<div class="chart-canvas-wrap">
|
| 26 |
+
<canvas id="chart-cer-doc" role="img" aria-label="CER par document" data-a11y-label="CER par document"></canvas>
|
| 27 |
</div>
|
| 28 |
</div>
|
| 29 |
|
| 30 |
<div class="chart-card">
|
| 31 |
<h3 data-i18n="h_duration">Temps d'exécution moyen (secondes/document)</h3>
|
| 32 |
<div class="chart-canvas-wrap">
|
| 33 |
+
<canvas id="chart-duration" role="img" aria-label="Durée d'inférence par moteur" data-a11y-label="Durée d'inférence par moteur"></canvas>
|
| 34 |
</div>
|
| 35 |
</div>
|
| 36 |
|
| 37 |
<div class="chart-card">
|
| 38 |
<h3 data-i18n="h_quality_cer">Qualité image ↔ CER (scatter plot)</h3>
|
| 39 |
<div class="chart-canvas-wrap">
|
| 40 |
+
<canvas id="chart-quality-cer" role="img" aria-label="Corrélation qualité d'image / CER" data-a11y-label="Corrélation qualité d'image / CER"></canvas>
|
| 41 |
</div>
|
| 42 |
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="quality_cer_note">
|
| 43 |
Chaque point = un document. Axe X = score qualité image [0–1]. Axe Y = CER. Corrélation négative attendue.
|
|
|
|
| 47 |
<div class="chart-card" style="grid-column:1/-1">
|
| 48 |
<h3 data-i18n="h_taxonomy">Taxonomie des erreurs par moteur</h3>
|
| 49 |
<div class="chart-canvas-wrap" style="max-height:300px">
|
| 50 |
+
<canvas id="chart-taxonomy" role="img" aria-label="Taxonomie d'erreurs par moteur" data-a11y-label="Taxonomie d'erreurs par moteur"></canvas>
|
| 51 |
</div>
|
| 52 |
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="taxonomy_note">
|
| 53 |
Distribution des classes d'erreurs (classes 1–9 de la taxonomie Picarones).
|
|
|
|
| 58 |
<div class="chart-card" style="grid-column:1/-1">
|
| 59 |
<h3 data-i18n="h_reliability">Courbes de fiabilité</h3>
|
| 60 |
<div class="chart-canvas-wrap" style="max-height:300px">
|
| 61 |
+
<canvas id="chart-reliability" role="img" aria-label="Diagramme de fiabilité (calibration)" data-a11y-label="Diagramme de fiabilité (calibration)"></canvas>
|
| 62 |
</div>
|
| 63 |
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="reliability_note">
|
| 64 |
Pour les X% documents les plus faciles (triés par CER croissant), quel est le CER moyen cumulé ?
|
|
|
|
| 70 |
<div class="chart-card">
|
| 71 |
<h3 data-i18n="h_bootstrap">Intervalles de confiance à 95 % (bootstrap)</h3>
|
| 72 |
<div class="chart-canvas-wrap">
|
| 73 |
+
<canvas id="chart-bootstrap-ci" role="img" aria-label="Intervalles de confiance bootstrap" data-a11y-label="Intervalles de confiance bootstrap"></canvas>
|
| 74 |
</div>
|
| 75 |
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="bootstrap_note">
|
| 76 |
IC à 95% sur le CER moyen par moteur (1000 itérations bootstrap).
|
|
|
|
| 106 |
<div class="chart-card">
|
| 107 |
<h3 data-i18n="h_gini_cer">Gini vs CER moyen <span style="font-size:.72rem;font-weight:400;color:var(--text-muted)" data-i18n="gini_cer_ideal">— idéal : bas-gauche</span></h3>
|
| 108 |
<div class="chart-canvas-wrap">
|
| 109 |
+
<canvas id="chart-gini-cer" role="img" aria-label="Gini vs CER" data-a11y-label="Gini vs CER"></canvas>
|
| 110 |
</div>
|
| 111 |
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="gini_cer_note">
|
| 112 |
Axe X = CER moyen, Axe Y = coefficient de Gini. Un moteur idéal a CER bas ET Gini bas (erreurs rares et uniformes).
|
|
|
|
| 117 |
<div class="chart-card">
|
| 118 |
<h3 data-i18n="h_ratio_anchor">Ratio longueur vs ancrage <span style="font-size:.72rem;font-weight:400;color:var(--text-muted)" data-i18n="ratio_anchor_subtitle">— hallucinations VLM</span></h3>
|
| 119 |
<div class="chart-canvas-wrap">
|
| 120 |
+
<canvas id="chart-ratio-anchor" role="img" aria-label="Score d'ancrage par moteur" data-a11y-label="Score d'ancrage par moteur"></canvas>
|
| 121 |
</div>
|
| 122 |
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="ratio_anchor_note">
|
| 123 |
Axe X = score d'ancrage trigrammes [0–1]. Axe Y = ratio longueur sortie/GT.
|
|
|
|
| 137 |
onclick="setParetoAxis('co2')" data-i18n="pareto_axis_co2"
|
| 138 |
title="Estimation expérimentale">Carbone (g CO₂)</button>
|
| 139 |
</div>
|
| 140 |
+
<div class="chart-canvas-wrap"><canvas id="pareto-chart" role="img" aria-label="Front Pareto coût/qualité" data-a11y-label="Front Pareto coût/qualité"></canvas></div>
|
| 141 |
<div id="pareto-method-note" class="pareto-note" data-i18n="pareto_note">
|
| 142 |
Les moteurs sur la frontière de Pareto (en évidence) sont ceux pour
|
| 143 |
lesquels aucun autre moteur n'offre simultanément un meilleur CER ET
|
|
@@ -8,22 +8,22 @@
|
|
| 8 |
<table id="ranking-table">
|
| 9 |
<thead>
|
| 10 |
<tr>
|
| 11 |
-
<th data-col="rank" class="sortable sorted" data-dir="asc" data-i18n="col_rank">#<i class="sort-icon">↑</i></th>
|
| 12 |
-
<th data-col="name" class="sortable" data-i18n="col_engine">Concurrent<i class="sort-icon">↕</i></th>
|
| 13 |
-
<th data-col="cer" class="sortable" data-glossary-key="cer" data-i18n="col_cer">CER exact<i class="sort-icon">↕</i></th>
|
| 14 |
-
<th data-col="cer_diplomatic" class="sortable" id="th-cer-diplo" data-glossary-key="cer_diplomatic" data-i18n="col_cer_diplo">CER diplo.<i class="sort-icon">↕</i></th>
|
| 15 |
-
<th data-col="wer" class="sortable" data-glossary-key="wer" data-i18n="col_wer">WER<i class="sort-icon">↕</i></th>
|
| 16 |
-
<th data-col="mer" class="sortable" data-glossary-key="mer" data-i18n="col_mer">MER<i class="sort-icon">↕</i></th>
|
| 17 |
-
<th data-col="wil" class="sortable" data-glossary-key="wil" data-i18n="col_wil">WIL<i class="sort-icon">↕</i></th>
|
| 18 |
-
<th data-col="ligature_score" class="sortable" id="th-ligatures" data-glossary-key="ligature_score" data-i18n="col_ligatures">Ligatures<i class="sort-icon">↕</i></th>
|
| 19 |
-
<th data-col="diacritic_score" class="sortable" id="th-diacritics" data-glossary-key="diacritic_score" data-i18n="col_diacritics">Diacritiques<i class="sort-icon">↕</i></th>
|
| 20 |
-
<th data-col="gini" class="sortable" id="th-gini" data-glossary-key="gini" data-i18n="col_gini">Gini<i class="sort-icon">↕</i></th>
|
| 21 |
-
<th data-col="anchor_score" class="sortable" id="th-anchor" data-glossary-key="anchor_score" data-i18n="col_anchor">Ancrage<i class="sort-icon">↕</i></th>
|
| 22 |
-
<th data-i18n="col_cer_median">CER médian</th>
|
| 23 |
-
<th data-i18n="col_cer_min">CER min</th>
|
| 24 |
-
<th data-i18n="col_cer_max">CER max</th>
|
| 25 |
-
<th id="th-overnorm" data-i18n="col_overnorm">Sur-norm.</th>
|
| 26 |
-
<th data-i18n="col_docs">Docs</th>
|
| 27 |
</tr>
|
| 28 |
</thead>
|
| 29 |
<tbody id="ranking-tbody"></tbody>
|
|
|
|
| 8 |
<table id="ranking-table">
|
| 9 |
<thead>
|
| 10 |
<tr>
|
| 11 |
+
<th scope="col" data-col="rank" class="sortable sorted" data-dir="asc" data-i18n="col_rank">#<i class="sort-icon">↑</i></th>
|
| 12 |
+
<th scope="col" data-col="name" class="sortable" data-i18n="col_engine">Concurrent<i class="sort-icon">↕</i></th>
|
| 13 |
+
<th scope="col" data-col="cer" class="sortable" data-glossary-key="cer" data-i18n="col_cer">CER exact<i class="sort-icon">↕</i></th>
|
| 14 |
+
<th scope="col" data-col="cer_diplomatic" class="sortable" id="th-cer-diplo" data-glossary-key="cer_diplomatic" data-i18n="col_cer_diplo">CER diplo.<i class="sort-icon">↕</i></th>
|
| 15 |
+
<th scope="col" data-col="wer" class="sortable" data-glossary-key="wer" data-i18n="col_wer">WER<i class="sort-icon">↕</i></th>
|
| 16 |
+
<th scope="col" data-col="mer" class="sortable" data-glossary-key="mer" data-i18n="col_mer">MER<i class="sort-icon">↕</i></th>
|
| 17 |
+
<th scope="col" data-col="wil" class="sortable" data-glossary-key="wil" data-i18n="col_wil">WIL<i class="sort-icon">↕</i></th>
|
| 18 |
+
<th scope="col" data-col="ligature_score" class="sortable" id="th-ligatures" data-glossary-key="ligature_score" data-i18n="col_ligatures">Ligatures<i class="sort-icon">↕</i></th>
|
| 19 |
+
<th scope="col" data-col="diacritic_score" class="sortable" id="th-diacritics" data-glossary-key="diacritic_score" data-i18n="col_diacritics">Diacritiques<i class="sort-icon">↕</i></th>
|
| 20 |
+
<th scope="col" data-col="gini" class="sortable" id="th-gini" data-glossary-key="gini" data-i18n="col_gini">Gini<i class="sort-icon">↕</i></th>
|
| 21 |
+
<th scope="col" data-col="anchor_score" class="sortable" id="th-anchor" data-glossary-key="anchor_score" data-i18n="col_anchor">Ancrage<i class="sort-icon">↕</i></th>
|
| 22 |
+
<th scope="col" data-i18n="col_cer_median">CER médian</th>
|
| 23 |
+
<th scope="col" data-i18n="col_cer_min">CER min</th>
|
| 24 |
+
<th scope="col" data-i18n="col_cer_max">CER max</th>
|
| 25 |
+
<th scope="col" id="th-overnorm" data-i18n="col_overnorm">Sur-norm.</th>
|
| 26 |
+
<th scope="col" data-i18n="col_docs">Docs</th>
|
| 27 |
</tr>
|
| 28 |
</thead>
|
| 29 |
<tbody id="ranking-tbody"></tbody>
|
|
@@ -139,7 +139,7 @@ def build_throughput_html(
|
|
| 139 |
]
|
| 140 |
for col in (h_engine, h_raw, h_effective, h_drag, h_pages, h_errors):
|
| 141 |
parts.append(
|
| 142 |
-
f'<th style="padding:.4rem .6rem;text-align:left;'
|
| 143 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 144 |
f'{_e(col)}</th>'
|
| 145 |
)
|
|
|
|
| 139 |
]
|
| 140 |
for col in (h_engine, h_raw, h_effective, h_drag, h_pages, h_errors):
|
| 141 |
parts.append(
|
| 142 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 143 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 144 |
f'{_e(col)}</th>'
|
| 145 |
)
|
|
@@ -115,7 +115,7 @@ def build_worst_lines_table_html(
|
|
| 115 |
cols.append(diff_label)
|
| 116 |
for col in cols:
|
| 117 |
parts.append(
|
| 118 |
-
f'<th style="padding:.3rem .5rem;text-align:left;'
|
| 119 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 120 |
f'{_e(col)}</th>'
|
| 121 |
)
|
|
|
|
| 115 |
cols.append(diff_label)
|
| 116 |
for col in cols:
|
| 117 |
parts.append(
|
| 118 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 119 |
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 120 |
f'{_e(col)}</th>'
|
| 121 |
)
|
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests Sprint A6 — accessibilité WCAG niveau A bloquant.
|
| 2 |
+
|
| 3 |
+
Items B-9, B-10, m-3, m-4 de l'audit institutional-readiness-2026-05.
|
| 4 |
+
|
| 5 |
+
Ce fichier valide le **socle a11y bloquant** pour une déclaration de
|
| 6 |
+
conformité RGAA / WCAG 2.1 niveau A :
|
| 7 |
+
|
| 8 |
+
- WCAG 2.4.1 (Bypass Blocks) — skip-to-content link (B-10)
|
| 9 |
+
- WCAG 1.1.1 (Non-text Content) — Canvas charts → aria-label + table
|
| 10 |
+
jumelle accessible aux AT (B-9)
|
| 11 |
+
- WCAG 1.3.1 (Info and Relationships) — ``scope="col"`` sur les
|
| 12 |
+
``<th>`` (m-4)
|
| 13 |
+
- Pas de chaîne hardcodée FR/EN dans la nav (m-3)
|
| 14 |
+
|
| 15 |
+
Ces tests se contentent de vérifier la présence des marqueurs HTML
|
| 16 |
+
attendus dans le rapport généré. L'audit sémantique complet (NVDA /
|
| 17 |
+
JAWS / VoiceOver) reste manuel et tracé dans
|
| 18 |
+
``docs/audits/external-audits-2026/`` (Sprint A15).
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import re
|
| 24 |
+
|
| 25 |
+
import pytest
|
| 26 |
+
|
| 27 |
+
from picarones.fixtures import generate_sample_benchmark
|
| 28 |
+
from picarones.report.generator import ReportGenerator
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@pytest.fixture(scope="module")
|
| 32 |
+
def demo_html(tmp_path_factory) -> str:
|
| 33 |
+
"""Rapport démo (FR) généré une fois pour tous les tests du module."""
|
| 34 |
+
out = tmp_path_factory.mktemp("a11y") / "report.html"
|
| 35 |
+
bench = generate_sample_benchmark(n_docs=4)
|
| 36 |
+
ReportGenerator(bench, lang="fr").generate(out)
|
| 37 |
+
return out.read_text(encoding="utf-8")
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@pytest.fixture(scope="module")
|
| 41 |
+
def demo_html_en(tmp_path_factory) -> str:
|
| 42 |
+
"""Rapport démo (EN) — pour vérifier que les libellés a11y sont
|
| 43 |
+
bilingues."""
|
| 44 |
+
out = tmp_path_factory.mktemp("a11y_en") / "report_en.html"
|
| 45 |
+
bench = generate_sample_benchmark(n_docs=4)
|
| 46 |
+
ReportGenerator(bench, lang="en").generate(out)
|
| 47 |
+
return out.read_text(encoding="utf-8")
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# ---------------------------------------------------------------------------
|
| 51 |
+
# B-10 — Skip-to-content (WCAG 2.4.1)
|
| 52 |
+
# ---------------------------------------------------------------------------
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test_skip_link_present(demo_html: str) -> None:
|
| 56 |
+
"""Un lien ``href="#main"`` avec class ``skip-link`` doit exister."""
|
| 57 |
+
assert 'class="skip-link"' in demo_html
|
| 58 |
+
assert 'href="#main"' in demo_html
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def test_skip_link_first_focusable_in_body(demo_html: str) -> None:
|
| 62 |
+
"""Le skip-link doit être le **premier** élément focusable du body
|
| 63 |
+
(sinon Tab depuis l'URL bar atteint d'abord la nav, ce qui défait
|
| 64 |
+
le but)."""
|
| 65 |
+
body_start = demo_html.find("<body>")
|
| 66 |
+
assert body_start > 0
|
| 67 |
+
body_part = demo_html[body_start : body_start + 1500]
|
| 68 |
+
skip_pos = body_part.find('class="skip-link"')
|
| 69 |
+
nav_pos = body_part.find("<nav")
|
| 70 |
+
assert skip_pos > 0 and nav_pos > 0
|
| 71 |
+
assert skip_pos < nav_pos, (
|
| 72 |
+
"Le skip-link doit précéder le <nav> dans le DOM."
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def test_main_has_id_main(demo_html: str) -> None:
|
| 77 |
+
"""Le ``<main>`` doit avoir ``id="main"`` pour que le skip-link
|
| 78 |
+
pointe vers une cible existante."""
|
| 79 |
+
assert re.search(r'<main[^>]*\bid="main"', demo_html), (
|
| 80 |
+
'<main id="main"> attendu pour la cible du skip-link.'
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def test_skip_link_label_is_i18n(demo_html: str, demo_html_en: str) -> None:
|
| 85 |
+
"""Le libellé du skip-link doit être en français en mode FR et en
|
| 86 |
+
anglais en mode EN (pas de chaîne hardcodée)."""
|
| 87 |
+
# FR : "Aller au contenu"
|
| 88 |
+
assert "Aller au contenu" in demo_html
|
| 89 |
+
# EN : "Skip to content"
|
| 90 |
+
assert "Skip to content" in demo_html_en
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# ---------------------------------------------------------------------------
|
| 94 |
+
# B-9 — Canvas charts accessibles (WCAG 1.1.1)
|
| 95 |
+
# ---------------------------------------------------------------------------
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def test_all_canvases_have_aria_label(demo_html: str) -> None:
|
| 99 |
+
"""Tout ``<canvas>`` Chart.js (avec ``id="chart-..."`` ou
|
| 100 |
+
``pareto-chart``) doit avoir ``aria-label`` non vide.
|
| 101 |
+
|
| 102 |
+
Tolérance : un ``<canvas>`` créé dynamiquement côté JS sans id
|
| 103 |
+
pré-déclaré reste possible (Chart.js peut en générer pour des
|
| 104 |
+
sub-charts). Le test ne valide que les canvas que les templates
|
| 105 |
+
Jinja2 produisent — pas ceux du DOM dynamique."""
|
| 106 |
+
html = _strip_inline_scripts(demo_html)
|
| 107 |
+
canvases = re.findall(r"<canvas[^>]*>", html)
|
| 108 |
+
chart_canvases = [
|
| 109 |
+
c for c in canvases
|
| 110 |
+
if 'id="chart-' in c or 'id="pareto-chart"' in c
|
| 111 |
+
]
|
| 112 |
+
canvases_no_label = [
|
| 113 |
+
c for c in chart_canvases
|
| 114 |
+
if 'aria-label="' not in c and "data-a11y-label" not in c
|
| 115 |
+
]
|
| 116 |
+
assert not canvases_no_label, (
|
| 117 |
+
f"Canvas Chart.js sans aria-label : {canvases_no_label}"
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def test_canvases_have_role_img(demo_html: str) -> None:
|
| 122 |
+
"""``role="img"`` doit être posé sur les canvas pour les annoncer
|
| 123 |
+
comme images aux AT."""
|
| 124 |
+
canvases = re.findall(r"<canvas[^>]*>", demo_html)
|
| 125 |
+
chart_canvases = [c for c in canvases if "chart-" in c]
|
| 126 |
+
if not chart_canvases:
|
| 127 |
+
pytest.skip("Aucun canvas Chart.js dans le rapport démo")
|
| 128 |
+
canvases_no_role = [c for c in chart_canvases if 'role="img"' not in c]
|
| 129 |
+
assert not canvases_no_role, (
|
| 130 |
+
f"Canvas Chart.js sans role=img : {canvases_no_role[:3]}"
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def test_data_table_helpers_present(demo_html: str) -> None:
|
| 135 |
+
"""La fonction ``attachChartA11y`` qui génère les tables jumelles
|
| 136 |
+
doit être incluse dans le JS embarqué."""
|
| 137 |
+
assert "attachChartA11y" in demo_html
|
| 138 |
+
assert "_populateChartDataTable" in demo_html
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def test_view_data_button_label_localized(
|
| 142 |
+
demo_html: str, demo_html_en: str
|
| 143 |
+
) -> None:
|
| 144 |
+
"""Les libellés du bouton « Voir les données » doivent être dans
|
| 145 |
+
l'objet I18N côté JS (pas hardcodés en français)."""
|
| 146 |
+
assert "Voir les données" in demo_html
|
| 147 |
+
assert "View data" in demo_html_en
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
# ---------------------------------------------------------------------------
|
| 151 |
+
# m-4 — scope="col" sur les <th>
|
| 152 |
+
# ---------------------------------------------------------------------------
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def _strip_inline_scripts(html: str) -> str:
|
| 156 |
+
"""Retire les blocs ``<script>...</script>`` et ``<style>...</style>``
|
| 157 |
+
avant d'analyser les balises HTML.
|
| 158 |
+
|
| 159 |
+
Nécessaire car Chart.js minifié contient des chaînes comme
|
| 160 |
+
``<this._cachedMeta`` qui matchent le regex ``<th[\\s>]`` faussement
|
| 161 |
+
(sequence ``<t`` + word boundary). On limite l'analyse au HTML rendu
|
| 162 |
+
par les templates Jinja2, pas au JS embarqué.
|
| 163 |
+
"""
|
| 164 |
+
cleaned = re.sub(r"<script\b[^>]*>.*?</script>", "", html, flags=re.DOTALL)
|
| 165 |
+
cleaned = re.sub(r"<style\b[^>]*>.*?</style>", "", cleaned, flags=re.DOTALL)
|
| 166 |
+
return cleaned
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def test_table_headers_have_scope(demo_html: str) -> None:
|
| 170 |
+
"""Tout ``<th>`` rendu par les templates doit avoir ``scope="col"``
|
| 171 |
+
ou ``scope="row"``."""
|
| 172 |
+
html = _strip_inline_scripts(demo_html)
|
| 173 |
+
# Regex strict : <th suivi d'un espace ou >, qui n'a PAS d'attribut scope=
|
| 174 |
+
th_no_scope = re.findall(
|
| 175 |
+
r"<th(?:\s+(?![^>]*\bscope=)[^>]*)?>",
|
| 176 |
+
html,
|
| 177 |
+
)
|
| 178 |
+
# On filtre faux positifs : <thead, <tbody, <tfoot etc. ne doivent pas matcher.
|
| 179 |
+
th_no_scope = [t for t in th_no_scope if re.match(r"<th(\s|>)", t)]
|
| 180 |
+
total_th = len(re.findall(r"<th(\s|>)", html))
|
| 181 |
+
if total_th == 0:
|
| 182 |
+
pytest.skip("Pas de <th> dans le rapport démo")
|
| 183 |
+
assert not th_no_scope, (
|
| 184 |
+
f"{len(th_no_scope)}/{total_th} <th> sans scope= "
|
| 185 |
+
f"dans le HTML rendu (hors <script>/<style>). "
|
| 186 |
+
f"Premiers : {th_no_scope[:3]}"
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
# ---------------------------------------------------------------------------
|
| 191 |
+
# m-3 — Bouton Reset i18n
|
| 192 |
+
# ---------------------------------------------------------------------------
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def test_reset_button_uses_i18n_key(demo_html: str) -> None:
|
| 196 |
+
"""Le bouton « Réinitialiser » du bandeau d'exclusion doit avoir
|
| 197 |
+
``data-i18n="reset_all"`` (pas de chaîne FR hardcodée sans
|
| 198 |
+
mécanisme i18n)."""
|
| 199 |
+
# Le bouton apparaît avec data-i18n="reset_all"
|
| 200 |
+
assert 'data-i18n="reset_all"' in demo_html
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def test_reset_label_in_i18n_dicts(demo_html: str, demo_html_en: str) -> None:
|
| 204 |
+
"""Les clés ``reset_all`` doivent exister dans les deux
|
| 205 |
+
dictionnaires i18n embarqués."""
|
| 206 |
+
# Le JSON I18N est embarqué inline dans le HTML.
|
| 207 |
+
# On cherche un fragment JSON ``"reset_all":"..."``
|
| 208 |
+
assert re.search(r'"reset_all"\s*:\s*"R[ée]initialiser"', demo_html)
|
| 209 |
+
assert re.search(r'"reset_all"\s*:\s*"Reset"', demo_html_en)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
# ---------------------------------------------------------------------------
|
| 213 |
+
# Synthèse
|
| 214 |
+
# ---------------------------------------------------------------------------
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def test_html_has_lang_attribute(demo_html: str, demo_html_en: str) -> None:
|
| 218 |
+
"""``<html lang="...">`` doit être posé pour les AT (déjà cas mais
|
| 219 |
+
on renforce)."""
|
| 220 |
+
assert 'lang="fr"' in demo_html
|
| 221 |
+
assert 'lang="en"' in demo_html_en
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def test_global_a11y_smoke(demo_html: str) -> None:
|
| 225 |
+
"""Méta-test : tous les marqueurs a11y de niveau A sont présents
|
| 226 |
+
dans un rapport démo standard."""
|
| 227 |
+
markers = [
|
| 228 |
+
'class="skip-link"',
|
| 229 |
+
'href="#main"',
|
| 230 |
+
'id="main"',
|
| 231 |
+
'role="img"',
|
| 232 |
+
"attachChartA11y",
|
| 233 |
+
'scope="col"',
|
| 234 |
+
'data-i18n="reset_all"',
|
| 235 |
+
]
|
| 236 |
+
missing = [m for m in markers if m not in demo_html]
|
| 237 |
+
assert not missing, f"Marqueurs WCAG niveau A manquants : {missing}"
|