auditmysite 1.7.3

Resource-efficient WCAG 2.2 AA Accessibility Checker in Rust
Documentation
//! WCAG coverage section for PDF reports (issue #37).

use crate::output::localized::is_english;
use renderreport::components::advanced::{
    ChecklistPanel, ChecklistRow, KeyValueList, SectionHeaderSplit,
};
use renderreport::components::text::Label;
use renderreport::components::TagCloud;

use crate::audit::{AccessibilityScorer, AuditReport, CoverageRatio};
use crate::i18n::I18n;

/// Render a WCAG coverage section (issue #37).
///
/// Lists the criteria this tool automatically checks, plus the criteria
/// that fundamentally require manual review. Communicates audit scope
/// transparently so users avoid a false sense of security.
pub(super) fn render_wcag_coverage_section(
    mut builder: renderreport::engine::ReportBuilder,
    report: &AuditReport,
    i18n: &I18n,
) -> renderreport::engine::ReportBuilder {
    use crate::wcag::coverage::{automated_criteria, coverage_stats, manual_review_criteria};

    let en = is_english(i18n);

    // Principle coverage — informative secondary indicator (#99).
    let coverage =
        AccessibilityScorer::calculate_coverage(&report.accessibility.wcag_results.violations);
    let cov_title = if en {
        "Principle coverage (criteria passed)"
    } else {
        "Prinzip-Abdeckung (bestandene Kriterien)"
    };
    let fmt_ratio =
        |r: &CoverageRatio| format!("{}/{} ({:.0} %)", r.passed, r.total, r.ratio * 100.0);
    let (p_perc, p_op, p_und, p_rob) = if en {
        ("Perceivable", "Operable", "Understandable", "Robust")
    } else {
        ("Wahrnehmbar", "Bedienbar", "Verständlich", "Robust")
    };
    builder = builder.add_component(
        KeyValueList::new()
            .with_title(cov_title)
            .add(p_perc, fmt_ratio(&coverage.perceivable))
            .add(p_op, fmt_ratio(&coverage.operable))
            .add(p_und, fmt_ratio(&coverage.understandable))
            .add(p_rob, fmt_ratio(&coverage.robust)),
    );
    let (automated, total) = coverage_stats();
    // Criteria this tool checks automatically that WCAG 2.2 added (they didn't
    // exist in WCAG 2.1). They count toward `automated`/`total` like any other
    // A/AA criterion (plan 54 §1), but stay marked: EN 301 549 V3.2.1 — the
    // standard the BFSG references, mapped in this report's own appendix — is
    // built on WCAG 2.1 and does not cover them (#572).
    //
    // Nur A/AA: `automated` zählt keine AAA-Kriterien, und 2.4.12 ist sowohl
    // AAA als auch neu in 2.2 — es als "N davon" in die A/AA-Quote zu zählen
    // ergäbe wieder eine Zahl, die sich mit ihrer Bezugsgröße nicht deckt
    // (dieselbe Klasse Fehler wie in Plan 36 §1).
    let wcag22_criteria: Vec<&str> = automated_criteria()
        .iter()
        .filter(|(id, level)| *level != "AAA" && crate::wcag::coverage::is_wcag22_only(id))
        .map(|(id, _)| *id)
        .collect();
    let title = if en { "Audit scope" } else { "Prüfumfang" };
    let intro = if wcag22_criteria.is_empty() {
        if en {
            format!(
                "This audit covers {automated} of ~{total} testable WCAG 2.2 AA criteria automatically. The criteria listed below require manual review."
            )
        } else {
            format!(
                "Dieses Audit prüft {automated} von ca. {total} WCAG-2.2-AA-Kriterien automatisch. Die unten aufgeführten Kriterien benötigen manuelle Prüfung."
            )
        }
    } else {
        let wcag22_list = wcag22_criteria.join(", ");
        if en {
            format!(
                "This audit covers {automated} of ~{total} testable WCAG 2.2 AA criteria automatically. {} of them ({wcag22_list}, marked \"WCAG 2.2\" below and on individual findings) were added by WCAG 2.2 and are therefore not part of EN 301 549 V3.2.1. The criteria listed below require manual review.",
                wcag22_criteria.len()
            )
        } else {
            format!(
                "Dieses Audit prüft {automated} von ca. {total} WCAG-2.2-AA-Kriterien automatisch. {} davon ({wcag22_list}, unten und bei einzelnen Befunden mit „WCAG 2.2\" gekennzeichnet) sind mit WCAG 2.2 hinzugekommen und deshalb nicht Teil von EN 301 549 V3.2.1. Die unten aufgeführten Kriterien benötigen manuelle Prüfung.",
                wcag22_criteria.len()
            )
        }
    };

    builder = builder.add_component(SectionHeaderSplit::new(title, &intro).with_level(2));

    // The cloud lists every criterion this tool checks; `automated` counts
    // only the A/AA ones the ratio above is scoped to. Titling the cloud with
    // `automated` put "Automatisch geprüft (36)" above 56 entries, with
    // nothing reconciling the two numbers (plan 36 §1).
    let listed = automated_criteria().len();
    // Two disjoint groups, so they add up to `listed`.
    let aaa_listed = automated_criteria()
        .iter()
        .filter(|(_, level)| *level == "AAA")
        .count();
    debug_assert_eq!(
        automated + aaa_listed,
        listed,
        "the two groups must partition the listed criteria"
    );
    let automated_title = if en {
        format!("Automatically checked ({listed})")
    } else {
        format!("Automatisch geprüft ({listed})")
    };
    let listed_note = if en {
        format!(
            "{listed} criteria in total: the {automated} WCAG 2.2 A/AA criteria the ratio above is scoped to, plus {aaa_listed} AAA criteria this tool also checks."
        )
    } else {
        format!(
            "{listed} Kriterien insgesamt: die {automated} WCAG-2.2-A/AA-Kriterien, auf die sich die Quote oben bezieht, dazu {aaa_listed} AAA-Kriterien, die dieses Werkzeug ebenfalls prüft."
        )
    };
    let mut tag_cloud = TagCloud::new().with_title(&automated_title).with_gap("5pt");
    for (c, l) in automated_criteria().iter() {
        let tag = if crate::wcag::coverage::is_wcag22_only(c) {
            format!("WCAG {} ({}, WCAG 2.2)", c, l)
        } else {
            format!("WCAG {} ({})", c, l)
        };
        tag_cloud = tag_cloud.add(tag, "good");
    }
    builder = builder.add_component(tag_cloud);
    builder = builder.add_component(
        Label::new(listed_note)
            .with_size("8.8pt")
            .with_color(crate::output::pdf::design::tokens::MUTED),
    );

    let manual_title = if en {
        format!(
            "Requires manual review ({})",
            manual_review_criteria().len()
        )
    } else {
        format!(
            "Manuelle Prüfung erforderlich ({})",
            manual_review_criteria().len()
        )
    };
    let mut manual_cloud = TagCloud::new().with_title(&manual_title).with_gap("5pt");
    for (c, l, name) in manual_review_criteria().iter() {
        let localized_name = if en {
            *name
        } else {
            crate::wcag::coverage::manual_review_criterion_name_de(c, name)
        };
        manual_cloud = manual_cloud.add(format!("{c} ({l}) – {localized_name}"), "info");
    }
    builder = builder.add_component(manual_cloud);

    // Practical testing guide — how to test the manual criteria above
    let how_title = if en {
        "How to test manually"
    } else {
        "So testen Sie manuell"
    };
    let items: &[(&str, &str)] = if en {
        &[
            (
                "Keyboard navigation",
                "Tab through the entire page. No focus loss, no keyboard trap, every interactive element reachable.",
            ),
            (
                "Screen reader",
                "Test with NVDA/JAWS (Windows) or VoiceOver (Mac/iOS) — landmark navigation and form interaction.",
            ),
            (
                "400% zoom",
                "At 400% browser zoom: page operable without horizontal scrolling, no content lost.",
            ),
            (
                "Reduced motion",
                "Enable the OS 'reduce motion' setting and verify animations are disabled or significantly diminished.",
            ),
            (
                "Modal / dropdown interaction",
                "Full keyboard interaction: Tab, Enter, Space, Escape, Arrow keys. Focus returns to the trigger on close.",
            ),
            (
                "Color blindness simulation",
                "Use a tool like 'Color Oracle' to verify information is conveyed by more than color alone.",
            ),
        ]
    } else {
        &[
            (
                "Tastaturnavigation",
                "Komplette Seite per Tab navigieren. Kein Fokus verloren, kein Keyboard-Trap, jedes interaktive Element erreichbar.",
            ),
            (
                "Screenreader",
                "Test mit NVDA/JAWS (Windows) oder VoiceOver (Mac/iOS) — Landmark-Navigation und Formular-Interaktion.",
            ),
            (
                "400% Zoom",
                "Bei 400% Browser-Zoom: Seite ohne horizontales Scrollen bedienbar, kein Inhalt verloren.",
            ),
            (
                "Reduced Motion",
                "Betriebssystem-Einstellung „Bewegung reduzieren\" aktivieren und prüfen, ob Animationen deaktiviert oder reduziert werden.",
            ),
            (
                "Modal- / Dropdown-Interaktion",
                "Vollständige Tastaturbedienung: Tab, Enter, Space, Escape, Pfeiltasten. Fokus kehrt nach Schließen zum Trigger zurück.",
            ),
            (
                "Farbenblindheit",
                "Mit einem Werkzeug wie „Color Oracle\" prüfen, ob Informationen nicht ausschließlich über Farbe vermittelt werden.",
            ),
        ]
    };
    let rows: Vec<ChecklistRow> = items
        .iter()
        .map(|(t, d)| ChecklistRow::new(*t, *d).with_status("info"))
        .collect();
    builder.add_component(ChecklistPanel::new(rows).with_title(how_title))
}