car_browser/perception/
signals.rs1use super::ui_map::PageSignals;
4use crate::models::A11yNode;
5
6pub struct SignalDetector;
8
9impl SignalDetector {
10 pub fn new() -> Self {
11 Self
12 }
13
14 pub fn detect(&self, nodes: &[A11yNode]) -> PageSignals {
16 let mut signals = PageSignals::default();
17
18 for node in nodes {
19 let role = node.role.to_lowercase();
20 let name_lower = node
21 .name
22 .as_deref()
23 .map(|n| n.to_lowercase())
24 .unwrap_or_default();
25
26 if matches!(role.as_str(), "dialog" | "alertdialog" | "sheet") {
28 signals.modal_present = true;
29 }
30
31 if name_lower.contains("cookie")
33 || name_lower.contains("consent")
34 || name_lower.contains("accept all")
35 {
36 signals.cookie_banner = true;
37 }
38
39 if role == "alert" || name_lower.contains("error") || name_lower.contains("failed") {
41 signals.error_banner = true;
42 }
43
44 if role == "progressbar"
46 || name_lower.contains("loading")
47 || name_lower.contains("spinner")
48 {
49 signals.loading_indicator = true;
50 }
51
52 if signals.page_type_hint.is_none() {
54 if name_lower.contains("sign in")
55 || name_lower.contains("log in")
56 || name_lower.contains("password")
57 {
58 signals.page_type_hint = Some("login".to_string());
59 } else if name_lower.contains("checkout")
60 || name_lower.contains("payment")
61 || name_lower.contains("place order")
62 {
63 signals.page_type_hint = Some("checkout".to_string());
64 } else if name_lower.contains("search results")
65 || name_lower.contains("results for")
66 {
67 signals.page_type_hint = Some("search_results".to_string());
68 }
69 }
70 }
71
72 signals
73 }
74}
75
76impl Default for SignalDetector {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use crate::models::Bounds;
86
87 #[test]
88 fn test_detect_modal() {
89 let detector = SignalDetector::new();
90 let nodes = vec![A11yNode {
91 node_id: "n0".to_string(),
92 role: "dialog".to_string(),
93 name: Some("Confirm action".to_string()),
94 value: None,
95 bounds: Bounds::new(100.0, 100.0, 400.0, 300.0),
96 children: vec![],
97 focusable: false,
98 focused: false,
99 disabled: false,
100 }];
101
102 let signals = detector.detect(&nodes);
103 assert!(signals.modal_present);
104 }
105
106 #[test]
107 fn test_detect_login_page() {
108 let detector = SignalDetector::new();
109 let nodes = vec![A11yNode {
110 node_id: "n0".to_string(),
111 role: "button".to_string(),
112 name: Some("Sign In".to_string()),
113 value: None,
114 bounds: Bounds::new(100.0, 100.0, 80.0, 30.0),
115 children: vec![],
116 focusable: true,
117 focused: false,
118 disabled: false,
119 }];
120
121 let signals = detector.detect(&nodes);
122 assert_eq!(signals.page_type_hint.as_deref(), Some("login"));
123 }
124}