use crate::cli::WcagLevel;
use crate::wcag::types::{RuleMetadata, Severity, Violation};
use chromiumoxide::Page;
use tracing::warn;
pub(super) const RULE_META: RuleMetadata = RuleMetadata {
id: "aria-prohibited-attr",
name: "ARIA Prohibited Attributes",
level: WcagLevel::A,
severity: Severity::High,
description: "ARIA attributes that are prohibited on a role must not be used",
help_url: "https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html",
axe_id: "aria-prohibited-attr",
tags: &["wcag2a", "wcag412", "cat.aria"],
};
const PROHIBITED_ATTRS: &[(&str, &[&str])] = &[
("presentation", &["aria-label", "aria-labelledby"]),
("none", &["aria-label", "aria-labelledby"]),
(
"generic",
&["aria-label", "aria-labelledby", "aria-roledescription"],
),
("code", &["aria-label", "aria-labelledby"]),
("emphasis", &["aria-label", "aria-labelledby"]),
("strong", &["aria-label", "aria-labelledby"]),
("subscript", &["aria-label", "aria-labelledby"]),
("superscript", &["aria-label", "aria-labelledby"]),
("deletion", &["aria-label", "aria-labelledby"]),
("insertion", &["aria-label", "aria-labelledby"]),
];
pub async fn check_aria_prohibited_attr_with_page(page: &Page) -> Vec<Violation> {
let explicit_role_selector = PROHIBITED_ATTRS
.iter()
.flat_map(|(role, attrs)| {
attrs
.iter()
.map(move |attr| format!("[role=\"{role}\"][{attr}]"))
})
.collect::<Vec<_>>()
.join(",");
let js = [
"(function() {",
crate::accessibility::js_helpers::CSS_SELECTOR_JS,
r#"
var issues = [];
var implicitSelector = [
'div:not([role])[aria-label]',
'div:not([role])[aria-labelledby]',
'div:not([role])[aria-roledescription]',
'span:not([role])[aria-label]',
'span:not([role])[aria-labelledby]',
'span:not([role])[aria-roledescription]'
];
var selector = implicitSelector.join(',') + ',__EXPLICIT_ROLE_SELECTOR__';
var elements = document.querySelectorAll(selector);
for (var i = 0; i < elements.length; i++) {
var el = elements[i];
var s = window.getComputedStyle(el);
if (s.display === 'none' || s.visibility === 'hidden') continue;
var attrs = [];
['aria-label', 'aria-labelledby', 'aria-roledescription'].forEach(function(attr) {
if (el.hasAttribute(attr)) attrs.push(attr);
});
if (attrs.length === 0) continue;
issues.push({
selector: __amsCssSelector(el),
snippet: el.outerHTML.substring(0, 200),
attrs: attrs.join(', '),
// Without an explicit role only div/span match (see the
// selectors above); their implicit role is `generic` (HTML-AAM).
role: el.getAttribute('role') || 'generic'
});
}
return issues;
"#,
"})()",
]
.concat()
.replace("__EXPLICIT_ROLE_SELECTOR__", &explicit_role_selector);
let result = match page.evaluate(js.as_str()).await {
Ok(r) => r,
Err(e) => {
warn!("aria-prohibited-attr DOM JS failed: {}", e);
return vec![crate::wcag::technical_rule_failure_for(
"aria-prohibited-attr",
crate::cli::WcagLevel::A,
"page_evaluation_failed",
)];
}
};
let Some(value) = result.value() else {
return vec![crate::wcag::technical_rule_failure_for(
"aria-prohibited-attr",
crate::cli::WcagLevel::A,
"missing_evaluation_value",
)];
};
let Some(issues) = value.as_array() else {
return vec![];
};
issues
.iter()
.filter_map(|issue| {
let selector = issue.get("selector")?.as_str()?.to_string();
let attrs = issue.get("attrs")?.as_str()?.to_string();
let role = issue
.get("role")
.and_then(|v| v.as_str())
.unwrap_or("generic");
let mut violation = Violation::new(
RULE_META.id,
RULE_META.name,
RULE_META.level,
RULE_META.severity,
format!("ARIA attribute(s) '{attrs}' are prohibited on role '{role}'"),
&selector,
)
.with_selector(&selector)
.with_rule_id(RULE_META.axe_id)
.with_tags(RULE_META.tags.iter().map(|s| s.to_string()).collect())
.with_fix("Remove the prohibited ARIA attribute or add a valid semantic role.")
.with_help_url(RULE_META.help_url);
if let Some(snippet) = issue.get("snippet").and_then(|v| v.as_str()) {
violation = violation.with_html_snippet(snippet);
}
Some(violation)
})
.collect()
}