auditmysite 1.7.2

Resource-efficient WCAG 2.2 AA Accessibility Checker in Rust
Documentation
//! WCAG 2.4.2 Page Titled
//!
//! Web pages have titles that describe topic or purpose.
//! Level A
//!
//! Fehlender und leerer Titel laufen seit #690 als `document/title-missing`
//! bzw. `document/title-empty` im geteilten Bestand (siehe `wcag::shared`).
//! Hier bleibt nur die Heuristik für den nichtssagenden Titel („Untitled",
//! „Home"): Sie ist eigen und hat im geteilten Bestand kein Gegenstück.

use crate::cli::WcagLevel;
use crate::wcag::types::{RuleMetadata, Severity, Violation};
use chromiumoxide::Page;
use tracing::warn;

/// Rule metadata for 2.4.2
pub(super) const PAGE_TITLED_RULE: RuleMetadata = RuleMetadata {
    id: "2.4.2",
    name: "Page Titled",
    level: WcagLevel::A,
    severity: Severity::High,
    description: "Web pages have titles that describe topic or purpose",
    help_url: "https://www.w3.org/WAI/WCAG22/Understanding/page-titled.html",
    axe_id: "document-title",
    tags: &["wcag2a", "wcag242", "cat.text-alternatives"],
};

/// Nichtssagender Seitentitel, gelesen aus dem DOM: Der Wurzelname im
/// AX-Baum fällt ohne `<title>` auf die URL zurück und taugt dafür nicht.
///
/// Fehlt der Titel oder ist er leer, meldet diese Regel nichts — das ist
/// `document/title-*` aus dem geteilten Bestand, und ein zweiter Befund
/// stünde doppelt im Bericht.
pub async fn check_page_titled_with_page(page: &Page) -> Vec<Violation> {
    let result = match page
        .evaluate(
            "(function() { var title = document.querySelector('title'); return title ? title.textContent : ''; })()",
        )
        .await
    {
        Ok(r) => r,
        Err(e) => {
            warn!("document-title DOM JS failed: {}", e);
            return vec![crate::wcag::technical_rule_failure_for("document-title", crate::cli::WcagLevel::A, "page_evaluation_failed")];
        }
    };

    let title = result
        .value()
        .and_then(|value| value.as_str())
        .unwrap_or("")
        .trim();

    if title.is_empty() || !is_generic_title(title) {
        return vec![];
    }

    vec![Violation::new(
        PAGE_TITLED_RULE.id,
        PAGE_TITLED_RULE.name,
        PAGE_TITLED_RULE.level,
        Severity::High,
        "Page title is non-descriptive",
        "document",
    )
    .with_rule_id(PAGE_TITLED_RULE.axe_id)
    .with_selector("head")
    .with_tags(
        PAGE_TITLED_RULE
            .tags
            .iter()
            .map(|s| s.to_string())
            .collect(),
    )
    .with_fix("Replace the generic <title> with one that describes the page topic or purpose")
    .with_help_url(PAGE_TITLED_RULE.help_url)]
}

/// Check if a title is generic/non-descriptive
fn is_generic_title(title: &str) -> bool {
    let generic_titles = [
        "untitled",
        "untitled document",
        "new page",
        "home",
        "index",
        "page",
        "document",
        "welcome",
        "test",
        "localhost",
    ];

    let title_lower = title.to_lowercase();
    generic_titles.iter().any(|&g| title_lower == g)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_page_titled_rule_metadata() {
        assert_eq!(PAGE_TITLED_RULE.id, "2.4.2");
        assert_eq!(PAGE_TITLED_RULE.level, WcagLevel::A);
    }

    #[test]
    fn test_is_generic_title() {
        assert!(is_generic_title("Untitled"));
        assert!(is_generic_title("home"));
        assert!(is_generic_title("Index"));
        assert!(!is_generic_title("Product Details - My Store"));
    }
}