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_label_in_name_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_parsing_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_aria_allowed_attr, check_aria_naming_rules,
check_aria_prohibited_attr, check_aria_relationships, check_aria_required_attr,
check_aria_required_parent, check_aria_roles, check_bypass_blocks, check_dialog_rules,
check_error_identification, check_focus_order, check_focus_visible, check_form_rules,
check_headings, check_help, check_image_input_rules, check_info_relationships,
check_input_purpose, check_instructions, check_keyboard, check_label_in_name,
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_no_duplicate_main, check_landmark_unique, check_landmarks, check_language,
check_language_extended, check_link_purpose, check_link_purpose_link_only,
check_list_structure, check_media_alternative, check_media_rules, check_meta_viewport_large,
check_non_text_contrast, check_on_focus, check_on_input, check_page_titled, check_parsing,
check_region, check_resize_text, check_section_headings, check_server_side_image_map,
check_skip_link, check_status_messages, check_summary_name, check_svg_rules,
check_table_extended, check_table_rules, check_text_alternatives, check_unusual_words,
check_wcag22_rules, check_widget_rules,
};
use super::types::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 {
info!("Running WCAG checks at level {}", level);
let mut results = WcagResults::new();
results.nodes_checked = tree.len();
debug!("Running Level A rules...");
run_level_a_rules(tree, &mut results, filter);
if matches!(level, WcagLevel::AA | WcagLevel::AAA) {
debug!("Running Level AA rules...");
run_level_aa_rules(tree, &mut results, filter);
}
if level == WcagLevel::AAA {
debug!("Running Level AAA rules...");
run_level_aaa_rules(tree, &mut results, filter);
}
info!(
"WCAG check complete: {} violations found",
results.violations.len()
);
results
}
macro_rules! run_if_allowed {
($filter:expr, $axe_id:expr, $check_fn:expr, $results:expr, $tree:expr) => {
if $filter.should_run($axe_id) {
let rule_results = $check_fn($tree);
$results.merge(rule_results);
}
};
}
fn run_level_a_rules(tree: &AXTree, results: &mut WcagResults, filter: &RuleFilterConfig) {
run_if_allowed!(filter, "image-alt", check_text_alternatives, results, tree);
run_if_allowed!(filter, "area-alt", check_image_input_rules, results, tree);
run_if_allowed!(
filter,
"server-side-image-map",
check_server_side_image_map,
results,
tree
);
run_if_allowed!(
filter,
"definition-list",
check_info_relationships,
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, "document-title", check_page_titled, results, tree);
run_if_allowed!(filter, "link-name", check_link_purpose, results, tree);
run_if_allowed!(filter, "html-has-lang", check_language, results, tree);
run_if_allowed!(filter, "valid-lang", check_language_extended, 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,
"focus-order-semantics",
check_focus_order,
results,
tree
);
run_if_allowed!(
filter,
"label-content-name-mismatch",
check_label_in_name,
results,
tree
);
run_if_allowed!(
filter,
"focus-no-context-change",
check_on_focus,
results,
tree
);
run_if_allowed!(
filter,
"input-no-context-change",
check_on_input,
results,
tree
);
run_if_allowed!(filter, "label", check_labels, results, tree);
run_if_allowed!(filter, "aria-roles", check_aria_roles, results, tree);
run_if_allowed!(
filter,
"aria-allowed-attr",
check_aria_allowed_attr,
results,
tree
);
run_if_allowed!(
filter,
"aria-required-attr",
check_aria_required_attr,
results,
tree
);
run_if_allowed!(
filter,
"aria-required-parent",
check_aria_required_parent,
results,
tree
);
run_if_allowed!(
filter,
"aria-prohibited-attr",
check_aria_prohibited_attr,
results,
tree
);
run_if_allowed!(filter, "region", check_region, results, tree);
run_if_allowed!(filter, "aria-label", check_accessible_name, results, tree);
run_if_allowed!(
filter,
"aria-valid-attr",
check_aria_relationships,
results,
tree
);
run_if_allowed!(
filter,
"aria-command-name",
check_aria_naming_rules,
results,
tree
);
run_if_allowed!(
filter,
"table-duplicate-name",
check_table_rules,
results,
tree
);
run_if_allowed!(
filter,
"form-field-multiple-labels",
check_form_rules,
results,
tree
);
run_if_allowed!(filter, "list", check_list_structure, results, tree);
run_if_allowed!(filter, "dialog-name", check_dialog_rules, results, tree);
run_if_allowed!(
filter,
"aria-required-children",
check_widget_rules,
results,
tree
);
run_if_allowed!(filter, "video-caption", check_media_rules, results, tree);
run_if_allowed!(filter, "svg-img-alt", check_svg_rules, results, tree);
run_if_allowed!(filter, "skip-link", check_skip_link, results, tree);
run_if_allowed!(
filter,
"landmark-unique",
check_landmark_unique,
results,
tree
);
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,
tree
);
run_if_allowed!(
filter,
"landmark-no-duplicate-contentinfo",
check_landmark_no_duplicate_contentinfo,
results,
tree
);
run_if_allowed!(
filter,
"landmark-no-duplicate-main",
check_landmark_no_duplicate_main,
results,
tree
);
run_if_allowed!(
filter,
"td-headers-attr",
check_table_extended,
results,
tree
);
run_if_allowed!(filter, "summary-name", check_summary_name, results, tree);
run_if_allowed!(
filter,
"label-title-only",
check_label_title_only,
results,
tree
);
run_if_allowed!(filter, "duplicate-id-aria", check_parsing, results, tree);
run_if_allowed!(filter, "empty-heading", check_wcag22_rules, results, tree);
}
fn run_level_aa_rules(tree: &AXTree, results: &mut WcagResults, filter: &RuleFilterConfig) {
run_if_allowed!(
filter,
"autocomplete-valid",
check_input_purpose,
results,
tree
);
run_if_allowed!(filter, "meta-viewport", check_resize_text, results, tree);
run_if_allowed!(
filter,
"meta-viewport-large",
check_meta_viewport_large,
results,
tree
);
run_if_allowed!(
filter,
"non-text-contrast",
check_non_text_contrast,
results,
tree
);
run_if_allowed!(filter, "heading-order", check_headings, results, tree);
run_if_allowed!(filter, "focus-visible", check_focus_visible, results, tree);
run_if_allowed!(
filter,
"aria-live-region-role",
check_status_messages,
results,
tree
);
run_if_allowed!(filter, "landmark-one-main", check_landmarks, results, tree);
}
fn run_level_aaa_rules(tree: &AXTree, results: &mut WcagResults, 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, "media-alt", check_media_alternative, 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 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());
}
}