1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
//! WCAG 2.5.3 Label in Name
//!
//! For user interface components with labels that include text or images of text,
//! the name contains the text that is presented visually.
//! Level A
use crate::accessibility::AXTree;
use crate::wcag::types::WcagResults;
pub fn check_label_in_name(tree: &AXTree) -> WcagResults {
let mut results = WcagResults::new();
for node in tree.iter() {
if node.ignored {
continue;
}
let role = node.role.as_deref().unwrap_or("");
// Only check labeled interactive components
let is_labeled_component = matches!(
role,
"button"
| "link"
| "menuitem"
| "tab"
| "checkbox"
| "radio"
| "switch"
| "combobox"
| "textbox"
| "searchbox"
);
if !is_labeled_component {
continue;
}
// Skip nodes without an accessible name
if node.name.as_ref().is_none_or(|n| n.trim().is_empty()) {
continue;
}
// The visible label for interactive elements typically IS the accessible
// name. node.description often comes from title attributes which are NOT
// visible to users — comparing name against description causes false
// positives (e.g. logo links with title="Zur Startseite").
//
// Without access to the actual DOM text nodes (which the AXTree doesn't
// expose separately), we cannot reliably determine the "visible label"
// distinct from the accessible name. Pass for now.
results.passes += 1;
}
results
}
#[cfg(test)]
mod tests {
use super::*;
use crate::accessibility::{AXNode, AXTree};
fn labeled_node(id: &str, role: &str, name: &str, _description: Option<&str>) -> AXNode {
AXNode {
node_id: id.to_string(),
ignored: false,
ignored_reasons: vec![],
role: Some(role.to_string()),
name: Some(name.to_string()),
name_source: None,
description: _description.map(String::from),
value: None,
properties: vec![],
child_ids: vec![],
parent_id: None,
backend_dom_node_id: None,
}
}
#[test]
fn test_name_matches_label() {
let tree = AXTree::from_nodes(vec![labeled_node("1", "button", "Submit", Some("Submit"))]);
let results = check_label_in_name(&tree);
assert_eq!(results.violations.len(), 0);
}
#[test]
fn test_name_contains_label() {
let tree = AXTree::from_nodes(vec![labeled_node(
"1",
"button",
"Submit Form",
Some("Submit"),
)]);
let results = check_label_in_name(&tree);
assert_eq!(results.violations.len(), 0);
}
#[test]
fn test_title_attr_not_treated_as_visible_label() {
// title="Submit Form" is not visible — should not trigger a violation
let tree = AXTree::from_nodes(vec![labeled_node(
"1",
"button",
"Send",
Some("Submit Form"),
)]);
let results = check_label_in_name(&tree);
assert_eq!(results.violations.len(), 0);
}
#[test]
fn test_no_description_passes() {
let tree = AXTree::from_nodes(vec![labeled_node("1", "button", "Submit", None)]);
let results = check_label_in_name(&tree);
assert_eq!(results.violations.len(), 0);
}
}