use tracing::{debug, info};
pub use super::rules::{
check_abbreviations_with_page, check_background_audio_with_page,
check_click_handlers_with_page, check_content_on_hover_with_page,
check_focus_visible_css_with_page, check_identify_purpose_with_page, check_location_with_page,
check_motion_actuation_with_page, check_no_interruptions_with_page, check_no_timing_with_page,
check_orientation_with_page, check_pointer_cancellation_with_page,
check_pointer_gestures_with_page, check_re_authenticate_with_page,
check_reduced_motion_with_page, check_reflow_with_page, check_target_size_enhanced_with_page,
check_timeouts_with_page, check_timing_with_page, check_use_of_color_with_page,
check_visual_presentation_with_page,
};
use super::rules::{
check_accessible_name, check_bypass_blocks, check_error_identification, check_focus_visible,
check_form_rules, check_help, check_instructions, check_keyboard, check_label_title_only,
check_labels, check_landmark_banner_is_top_level, check_landmark_contentinfo_is_top_level,
check_landmark_main_is_top_level, check_landmark_no_duplicate_banner,
check_landmark_no_duplicate_contentinfo, check_landmark_unique, check_link_purpose,
check_link_purpose_link_only, check_media_rules, check_region, check_section_headings,
check_table_extended, check_text_alternatives, check_unusual_words,
};
use super::types::{Violation, WcagResults};
use crate::accessibility::AXTree;
use crate::cli::WcagLevel;
#[derive(Debug, Clone, Default)]
pub struct RuleFilterConfig {
pub disabled_rules: Vec<String>,
pub enabled_only_rules: Vec<String>,
}
impl RuleFilterConfig {
pub fn should_run(&self, axe_id: &str) -> bool {
if !self.enabled_only_rules.is_empty() {
return self.enabled_only_rules.iter().any(|r| r == axe_id);
}
!self.disabled_rules.iter().any(|r| r == axe_id)
}
}
pub fn check_all(tree: &AXTree, level: WcagLevel) -> WcagResults {
check_all_with_config(tree, level, &RuleFilterConfig::default())
}
pub fn check_all_with_config(
tree: &AXTree,
level: WcagLevel,
filter: &RuleFilterConfig,
) -> WcagResults {
check_all_excluding(tree, tree, level, filter, &|_| false).0
}
pub fn check_all_excluding(
tree: &AXTree,
counted: &AXTree,
level: WcagLevel,
filter: &RuleFilterConfig,
exclude: &dyn Fn(&Violation) -> bool,
) -> (WcagResults, Vec<Violation>) {
info!("Running WCAG checks at level {}", level);
let mut run = TreeRun {
results: WcagResults::new(),
exclude,
excluded: Vec::new(),
};
run.results.nodes_checked = tree.len();
debug!("Running Level A rules...");
run_level_a_rules(tree, counted, &mut run, filter);
if matches!(level, WcagLevel::AA | WcagLevel::AAA) {
debug!("Running Level AA rules...");
run_level_aa_rules(tree, &mut run, filter);
}
if level == WcagLevel::AAA {
debug!("Running Level AAA rules...");
run_level_aaa_rules(tree, &mut run, filter);
}
info!(
"WCAG check complete: {} violations found",
run.results.violations.len()
);
(run.results, run.excluded)
}
struct TreeRun<'a> {
results: WcagResults,
exclude: &'a dyn Fn(&Violation) -> bool,
excluded: Vec<Violation>,
}
impl TreeRun<'_> {
fn record(&mut self, axe_id: &str, mut rule_results: WcagResults) {
for list in [&mut rule_results.violations, &mut rule_results.warnings] {
let (dropped, kept): (Vec<_>, Vec<_>) = std::mem::take(list)
.into_iter()
.partition(|v| (self.exclude)(v));
*list = kept;
self.excluded.extend(dropped);
}
let finding_count = rule_results.violations.len();
let mut run = crate::wcag::RuleRun::ran(axe_id, finding_count);
if let Some(criterion) = crate::taxonomy::criterion_for_rule(axe_id) {
run = run.with_wcag([criterion]);
}
self.results.rule_outcomes.push(run);
self.results.merge(rule_results);
}
}
macro_rules! run_if_allowed {
($filter:expr, $axe_id:expr, $check_fn:expr, $results:expr, $tree:expr) => {
if $filter.should_run($axe_id) {
$results.record($axe_id, $check_fn($tree));
}
};
}
fn run_level_a_rules(
tree: &AXTree,
counted: &AXTree,
results: &mut TreeRun<'_>,
filter: &RuleFilterConfig,
) {
run_if_allowed!(filter, "image-alt", check_text_alternatives, results, tree);
run_if_allowed!(filter, "keyboard", check_keyboard, results, tree);
run_if_allowed!(filter, "bypass", check_bypass_blocks, results, tree);
run_if_allowed!(filter, "link-name", check_link_purpose, results, tree);
run_if_allowed!(filter, "label", check_instructions, results, tree);
run_if_allowed!(
filter,
"aria-invalid-without-describedby",
check_error_identification,
results,
tree
);
run_if_allowed!(filter, "label", check_labels, results, tree);
run_if_allowed!(filter, "region", check_region, results, tree);
run_if_allowed!(
filter,
"description-duplicates-name",
check_accessible_name,
results,
tree
);
run_if_allowed!(filter, "form-field-group", check_form_rules, results, tree);
run_if_allowed!(filter, "video-caption", check_media_rules, results, tree);
run_if_allowed!(
filter,
"landmark-unique",
check_landmark_unique,
results,
counted
);
run_if_allowed!(
filter,
"landmark-banner-is-top-level",
check_landmark_banner_is_top_level,
results,
tree
);
run_if_allowed!(
filter,
"landmark-contentinfo-is-top-level",
check_landmark_contentinfo_is_top_level,
results,
tree
);
run_if_allowed!(
filter,
"landmark-main-is-top-level",
check_landmark_main_is_top_level,
results,
tree
);
run_if_allowed!(
filter,
"landmark-no-duplicate-banner",
check_landmark_no_duplicate_banner,
results,
counted
);
run_if_allowed!(
filter,
"landmark-no-duplicate-contentinfo",
check_landmark_no_duplicate_contentinfo,
results,
counted
);
run_if_allowed!(
filter,
"th-has-data-cells",
check_table_extended,
results,
tree
);
run_if_allowed!(
filter,
"label-title-only",
check_label_title_only,
results,
tree
);
}
fn run_level_aa_rules(tree: &AXTree, results: &mut TreeRun<'_>, filter: &RuleFilterConfig) {
run_if_allowed!(filter, "focus-visible", check_focus_visible, results, tree);
}
fn run_level_aaa_rules(tree: &AXTree, results: &mut TreeRun<'_>, filter: &RuleFilterConfig) {
run_if_allowed!(
filter,
"heading-order",
check_section_headings,
results,
tree
);
run_if_allowed!(
filter,
"link-name",
check_link_purpose_link_only,
results,
tree
);
run_if_allowed!(filter, "unusual-words", check_unusual_words, results, tree);
run_if_allowed!(filter, "help", check_help, results, tree);
debug!("Level AAA rules executed");
}
#[cfg(test)]
mod tests {
use super::*;
use crate::accessibility::{AXNode, AXTree};
fn create_test_tree() -> AXTree {
let nodes = vec![
AXNode {
node_id: "1".to_string(),
ignored: false,
ignored_reasons: vec![],
role: Some("WebArea".to_string()),
name: Some("Test Page".to_string()),
name_source: None,
description: None,
value: None,
properties: vec![],
child_ids: vec!["2".to_string()],
parent_id: None,
backend_dom_node_id: None,
},
AXNode {
node_id: "2".to_string(),
ignored: false,
ignored_reasons: vec![],
role: Some("image".to_string()),
name: None, name_source: None,
description: None,
value: None,
properties: vec![],
child_ids: vec![],
parent_id: Some("1".to_string()),
backend_dom_node_id: None,
},
];
AXTree::from_nodes(nodes)
}
#[test]
fn test_check_all_level_a() {
let tree = create_test_tree();
let results = check_all(&tree, WcagLevel::A);
assert!(!results.violations.is_empty());
assert!(results.violations.iter().any(|v| v.rule == "1.1.1"));
}
#[test]
fn check_all_excluding_drops_per_rule_and_counts_what_remains() {
let tree = create_test_tree();
let full = check_all(&tree, WcagLevel::A);
let alt_run = |r: &WcagResults| {
r.rule_outcomes
.iter()
.find(|o| o.rule_id == "image-alt")
.map(|o| o.findings)
};
assert!(alt_run(&full).unwrap() > 0);
let (kept, dropped) = check_all_excluding(
&tree,
&tree,
WcagLevel::A,
&RuleFilterConfig::default(),
&|v| v.node_id == "2",
);
assert!(kept.violations.iter().all(|v| v.node_id != "2"));
assert!(dropped.iter().any(|v| v.rule == "1.1.1"));
assert_eq!(alt_run(&kept), Some(0));
}
#[test]
fn test_check_all_level_aa() {
let tree = create_test_tree();
let results = check_all(&tree, WcagLevel::AA);
assert!(!results.violations.is_empty());
}
#[test]
fn test_rule_filter_disabled_rules() {
let tree = create_test_tree();
let filter = RuleFilterConfig {
disabled_rules: vec!["image-alt".to_string()],
enabled_only_rules: vec![],
};
let results = check_all_with_config(&tree, WcagLevel::A, &filter);
assert!(results.violations.iter().all(|v| v.rule != "1.1.1"));
}
#[test]
fn test_rule_filter_wcag_level() {
let tree = create_test_tree();
let results_a = check_all(&tree, WcagLevel::A);
let results_aa = check_all(&tree, WcagLevel::AA);
assert!(results_aa.violations.len() >= results_a.violations.len());
let results_aaa = check_all(&tree, WcagLevel::AAA);
assert!(results_aaa.violations.len() >= results_aa.violations.len());
}
#[test]
fn test_rule_filter_default_runs_all() {
let tree = create_test_tree();
let default_filter = RuleFilterConfig::default();
let filtered = check_all_with_config(&tree, WcagLevel::A, &default_filter);
let unfiltered = check_all(&tree, WcagLevel::A);
assert_eq!(filtered.violations.len(), unfiltered.violations.len());
}
}