Skip to main content

car_browser/perception/
signals.rs

1//! Page signal detection from accessibility tree.
2
3use super::ui_map::PageSignals;
4use crate::models::A11yNode;
5
6/// Detects page-level signals from the accessibility tree.
7pub struct SignalDetector;
8
9impl SignalDetector {
10    pub fn new() -> Self {
11        Self
12    }
13
14    /// Detect page signals from accessibility nodes.
15    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            // Modal/dialog detection
27            if matches!(role.as_str(), "dialog" | "alertdialog" | "sheet") {
28                signals.modal_present = true;
29            }
30
31            // Cookie banner detection
32            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            // Error banner detection
40            if role == "alert" || name_lower.contains("error") || name_lower.contains("failed") {
41                signals.error_banner = true;
42            }
43
44            // Loading indicator detection
45            if role == "progressbar"
46                || name_lower.contains("loading")
47                || name_lower.contains("spinner")
48            {
49                signals.loading_indicator = true;
50            }
51
52            // Page type hints
53            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}