agent_tui/
wait.rs

1use crate::session::Session;
2use std::collections::hash_map::DefaultHasher;
3use std::hash::{Hash, Hasher};
4
5#[derive(Debug, Clone)]
6pub enum WaitCondition {
7    Text(String),
8    Element(String),
9    Focused(String),
10    NotVisible(String),
11    Stable,
12    TextGone(String),
13    Value { element: String, expected: String },
14}
15
16impl WaitCondition {
17    pub fn parse(
18        condition: Option<&str>,
19        target: Option<&str>,
20        text: Option<&str>,
21    ) -> Option<Self> {
22        match condition {
23            Some("text") => text.map(|t| WaitCondition::Text(t.to_string())),
24            Some("element") => target.map(|t| WaitCondition::Element(t.to_string())),
25            Some("focused") => target.map(|t| WaitCondition::Focused(t.to_string())),
26            Some("not_visible") => target.map(|t| WaitCondition::NotVisible(t.to_string())),
27            Some("stable") => Some(WaitCondition::Stable),
28            Some("text_gone") => target.map(|t| WaitCondition::TextGone(t.to_string())),
29            Some("value") => target.and_then(|t| {
30                let parts: Vec<&str> = t.splitn(2, '=').collect();
31                if parts.len() == 2 {
32                    Some(WaitCondition::Value {
33                        element: parts[0].to_string(),
34                        expected: parts[1].to_string(),
35                    })
36                } else {
37                    text.map(|expected_value| WaitCondition::Value {
38                        element: t.to_string(),
39                        expected: expected_value.to_string(),
40                    })
41                }
42            }),
43            None => text.map(|t| WaitCondition::Text(t.to_string())),
44            _ => None,
45        }
46    }
47
48    pub fn description(&self) -> String {
49        match self {
50            WaitCondition::Text(t) => format!("text \"{}\"", t),
51            WaitCondition::Element(e) => format!("element {}", e),
52            WaitCondition::Focused(e) => format!("{} to be focused", e),
53            WaitCondition::NotVisible(e) => format!("{} to disappear", e),
54            WaitCondition::Stable => "screen to stabilize".to_string(),
55            WaitCondition::TextGone(t) => format!("text \"{}\" to disappear", t),
56            WaitCondition::Value { element, expected } => {
57                format!("{} to have value \"{}\"", element, expected)
58            }
59        }
60    }
61}
62
63#[derive(Default)]
64pub struct StableTracker {
65    last_hashes: Vec<u64>,
66    required_consecutive: usize,
67}
68
69impl StableTracker {
70    pub fn new(required_consecutive: usize) -> Self {
71        Self {
72            last_hashes: Vec::new(),
73            required_consecutive,
74        }
75    }
76
77    pub fn add_hash(&mut self, screen: &str) -> bool {
78        let mut hasher = DefaultHasher::new();
79        screen.hash(&mut hasher);
80        let hash = hasher.finish();
81
82        self.last_hashes.push(hash);
83
84        if self.last_hashes.len() > self.required_consecutive {
85            self.last_hashes.remove(0);
86        }
87
88        if self.last_hashes.len() >= self.required_consecutive {
89            let first = self.last_hashes[0];
90            self.last_hashes.iter().all(|&h| h == first)
91        } else {
92            false
93        }
94    }
95}
96
97pub fn check_condition(
98    session: &mut Session,
99    condition: &WaitCondition,
100    stable_tracker: &mut StableTracker,
101) -> bool {
102    let _ = session.update();
103
104    match condition {
105        WaitCondition::Text(text) => {
106            let screen = session.screen_text();
107            screen.contains(text)
108        }
109
110        WaitCondition::Element(element_ref) => {
111            session.detect_elements();
112            session.find_element(element_ref).is_some()
113        }
114
115        WaitCondition::Focused(element_ref) => {
116            session.detect_elements();
117            session
118                .find_element(element_ref)
119                .map(|el| el.focused)
120                .unwrap_or(false)
121        }
122
123        WaitCondition::NotVisible(element_ref) => {
124            session.detect_elements();
125            session.find_element(element_ref).is_none()
126        }
127
128        WaitCondition::Stable => {
129            let screen = session.screen_text();
130            stable_tracker.add_hash(&screen)
131        }
132
133        WaitCondition::TextGone(text) => {
134            let screen = session.screen_text();
135            !screen.contains(text)
136        }
137
138        WaitCondition::Value { element, expected } => {
139            session.detect_elements();
140            session
141                .find_element(element)
142                .and_then(|el| el.value.as_ref())
143                .map(|v| v == expected)
144                .unwrap_or(false)
145        }
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_parse_text_condition() {
155        let cond = WaitCondition::parse(Some("text"), None, Some("hello"));
156        assert!(matches!(cond, Some(WaitCondition::Text(t)) if t == "hello"));
157    }
158
159    #[test]
160    fn test_parse_element_condition() {
161        let cond = WaitCondition::parse(Some("element"), Some("@btn1"), None);
162        assert!(matches!(cond, Some(WaitCondition::Element(e)) if e == "@btn1"));
163    }
164
165    #[test]
166    fn test_parse_stable_condition() {
167        let cond = WaitCondition::parse(Some("stable"), None, None);
168        assert!(matches!(cond, Some(WaitCondition::Stable)));
169    }
170
171    #[test]
172    fn test_parse_value_condition() {
173        let cond = WaitCondition::parse(Some("value"), Some("@inp1=hello"), None);
174        assert!(
175            matches!(cond, Some(WaitCondition::Value { element, expected }) if element == "@inp1" && expected == "hello")
176        );
177    }
178
179    #[test]
180    fn test_parse_value_condition_with_separate_text() {
181        let cond = WaitCondition::parse(Some("value"), Some("@e2"), Some("expected value"));
182        assert!(
183            matches!(cond, Some(WaitCondition::Value { element, expected }) if element == "@e2" && expected == "expected value")
184        );
185    }
186
187    #[test]
188    fn test_stable_tracker() {
189        let mut tracker = StableTracker::new(3);
190
191        assert!(!tracker.add_hash("screen1"));
192        assert!(!tracker.add_hash("screen2"));
193        assert!(!tracker.add_hash("screen3"));
194
195        assert!(!tracker.add_hash("stable"));
196        assert!(!tracker.add_hash("stable"));
197        assert!(tracker.add_hash("stable"));
198    }
199
200    #[test]
201    fn test_default_to_text_condition() {
202        let cond = WaitCondition::parse(None, None, Some("hello"));
203        assert!(matches!(cond, Some(WaitCondition::Text(t)) if t == "hello"));
204    }
205}