use chromiumoxide::Page;
use crate::cli::WcagLevel;
use crate::wcag::types::{evaluate_or_fail, RuleMetadata, Severity, Violation};
pub(super) const REDUNDANT_ROLE_RULE: RuleMetadata = RuleMetadata {
id: "4.1.2",
name: "Name, Role, Value",
level: WcagLevel::A,
severity: Severity::Low,
description:
"An explicit role attribute must not merely restate the element's own implicit role",
help_url: "https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html",
axe_id: "redundant-role",
tags: &["wcag2a", "wcag412", "cat.aria", "best-practice"],
};
const REDUNDANT_ROLE_JS: &str = r#"
function keepsListSemantics(el) {
return getComputedStyle(el).listStyleType !== 'none';
}
var MAP = [
['button', function(el) { return true; }, 'button'],
['a', function(el) { return el.hasAttribute('href'); }, 'link'],
['textarea', function(el) { return true; }, 'textbox'],
['input', function(el) {
var t = (el.getAttribute('type') || 'text').toLowerCase();
return t === 'text' || t === '';
}, 'textbox'],
['input', function(el) { return (el.getAttribute('type') || '').toLowerCase() === 'checkbox'; }, 'checkbox'],
['input', function(el) { return (el.getAttribute('type') || '').toLowerCase() === 'radio'; }, 'radio'],
['input', function(el) {
var t = (el.getAttribute('type') || '').toLowerCase();
return t === 'button' || t === 'submit' || t === 'reset';
}, 'button'],
['h1', function(el) { return true; }, 'heading'],
['h2', function(el) { return true; }, 'heading'],
['h3', function(el) { return true; }, 'heading'],
['h4', function(el) { return true; }, 'heading'],
['h5', function(el) { return true; }, 'heading'],
['h6', function(el) { return true; }, 'heading'],
['ul', keepsListSemantics, 'list'],
['ol', keepsListSemantics, 'list'],
['li', function(el) { return true; }, 'listitem'],
['table', function(el) { return true; }, 'table'],
['img', function(el) { return el.hasAttribute('alt') && el.getAttribute('alt') !== ''; }, 'img']
];
var findings = [];
var elements = document.querySelectorAll('[role]');
for (var i = 0; i < elements.length && __amsReal(findings) < 20; i++) {
var el = elements[i];
var tag = el.tagName.toLowerCase();
var role = (el.getAttribute('role') || '').trim().toLowerCase();
if (!role) continue;
for (var j = 0; j < MAP.length; j++) {
if (MAP[j][0] !== tag) continue;
if (MAP[j][2] !== role) continue;
if (!MAP[j][1](el)) continue;
__amsPush(findings, el, { selector: __amsCssSelector(el), role: role }, 20);
break;
}
}
return findings;
"#;
pub async fn check_redundant_role_with_page(page: &Page) -> Vec<Violation> {
let js = [
"(function() {",
crate::accessibility::js_helpers::CSS_SELECTOR_JS,
REDUNDANT_ROLE_JS,
"})()",
]
.concat();
let value = match evaluate_or_fail(page, &REDUNDANT_ROLE_RULE, &js).await {
Ok(v) => v,
Err(violations) => return violations,
};
let Some(issues) = value.as_array() else {
return vec![];
};
issues
.iter()
.filter_map(|issue| {
let selector = issue.get("selector")?.as_str()?.to_string();
let role = issue.get("role")?.as_str()?.to_string();
Some(
Violation::new(
REDUNDANT_ROLE_RULE.id,
REDUNDANT_ROLE_RULE.name,
REDUNDANT_ROLE_RULE.level,
REDUNDANT_ROLE_RULE.severity,
format!(
"role=\"{role}\" merely restates this element's own implicit role — some browser/AT combinations announce it twice."
),
&selector,
)
.with_selector(&selector)
.with_rule_id(REDUNDANT_ROLE_RULE.axe_id)
.with_tags(
REDUNDANT_ROLE_RULE
.tags
.iter()
.map(|s| s.to_string())
.collect(),
)
.with_fix("Remove the redundant role attribute; the native element already exposes this role.")
.with_help_url(REDUNDANT_ROLE_RULE.help_url),
)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rule_metadata_is_low_severity_best_practice() {
assert_eq!(REDUNDANT_ROLE_RULE.severity, Severity::Low);
assert!(REDUNDANT_ROLE_RULE.tags.contains(&"best-practice"));
}
}