use chromiumoxide::Page;
use crate::cli::WcagLevel;
use crate::wcag::types::{Outcome, RuleMetadata, Severity, Violation};
pub(super) const POINTER_CANCELLATION_RULE: RuleMetadata = RuleMetadata {
id: "2.5.2",
name: "Pointer Cancellation",
level: WcagLevel::A,
severity: Severity::Medium,
description: "Actions are not triggered on the down-event unless essential",
help_url: "https://www.w3.org/WAI/WCAG22/Understanding/pointer-cancellation.html",
axe_id: "pointer-cancellation",
tags: &["wcag2a", "wcag252", "cat.sensory-and-visual-cues"],
};
const POINTER_CANCELLATION_JS: &str = r#"
(function() {
var found = [];
var interactives = document.querySelectorAll('button, a[href], [role="button"], input[type="submit"], input[type="button"]');
for (var i = 0; i < Math.min(interactives.length, 100); i++) {
var el = interactives[i];
if (el.hasAttribute('onmousedown') || el.hasAttribute('ontouchstart')) {
var desc = el.getAttribute('aria-label') || el.textContent.trim().substring(0, 40) || el.tagName.toLowerCase();
found.push(desc);
}
}
return { found: found };
})()
"#;
pub async fn check_pointer_cancellation_with_page(page: &Page) -> Vec<Violation> {
let not_testable = Violation::new(
POINTER_CANCELLATION_RULE.id,
POINTER_CANCELLATION_RULE.name,
POINTER_CANCELLATION_RULE.level,
Severity::Low,
"WCAG 2.5.2 requires manual testing to verify that actions are not triggered \
on the down-event (or that undo/reversal mechanisms exist).",
"page",
)
.with_fix(
"Trigger actions on the up-event (mouseup/touchend/click) rather than the down-event \
(mousedown/touchstart), unless the down-event is essential to the function.",
)
.with_rule_id(POINTER_CANCELLATION_RULE.axe_id)
.with_help_url(POINTER_CANCELLATION_RULE.help_url)
.with_kind(Outcome::Untested);
let result = match page.evaluate(POINTER_CANCELLATION_JS).await {
Ok(r) => r,
Err(_) => return vec![not_testable],
};
let val = match result.value() {
Some(v) => v.clone(),
None => return vec![not_testable],
};
let found: Vec<String> = val
.get("found")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let mut findings = vec![not_testable];
if !found.is_empty() {
findings.push(
Violation::new(
POINTER_CANCELLATION_RULE.id,
POINTER_CANCELLATION_RULE.name,
POINTER_CANCELLATION_RULE.level,
Severity::Medium,
format!(
"Interactive elements with onmousedown/ontouchstart inline handlers found: \
'{}'. These may trigger actions on pointer down, preventing cancellation.",
found.join("', '")
),
"button, a[href]",
)
.with_fix(
"Use onclick or onmouseup/ontouchend instead, or ensure the action can \
be cancelled by moving the pointer off the element before release.",
)
.with_rule_id(POINTER_CANCELLATION_RULE.axe_id)
.with_help_url(POINTER_CANCELLATION_RULE.help_url)
.with_kind(Outcome::Review),
);
}
findings
}