browser_commander/high_level/
universal_logic.rs1use crate::core::engine::{EngineAdapter, EngineError};
6use crate::core::navigation::is_navigation_error;
7use std::time::Duration;
8
9pub async fn wait_for_url_condition<F>(
23 adapter: &dyn EngineAdapter,
24 target_url: &str,
25 _description: Option<&str>,
26 polling_interval: Duration,
27 custom_check: Option<F>,
28) -> Result<bool, EngineError>
29where
30 F: Fn(&str) -> bool,
31{
32 loop {
33 if let Some(ref check) = custom_check {
35 let current_url = adapter.url().await?;
36 if check(¤t_url) {
37 return Ok(true);
38 }
39 }
40
41 let current_url = match adapter.url().await {
43 Ok(url) => url,
44 Err(e) if is_navigation_error(&e.to_string()) => {
45 tokio::time::sleep(polling_interval).await;
47 continue;
48 }
49 Err(e) => return Err(e),
50 };
51
52 if current_url.starts_with(target_url) {
53 return Ok(true);
54 }
55
56 tokio::time::sleep(polling_interval).await;
57 }
58}
59
60pub async fn install_click_listener(
75 adapter: &dyn EngineAdapter,
76 button_text: &str,
77 storage_key: &str,
78) -> Result<bool, EngineError> {
79 let script = format!(
80 r#"
81 (function() {{
82 document.addEventListener('click', (event) => {{
83 let element = event.target;
84 while (element && element !== document.body) {{
85 const elementText = element.textContent?.trim() || '';
86 if (elementText === '{}' ||
87 ((element.tagName === 'A' || element.tagName === 'BUTTON') &&
88 elementText.includes('{}'))) {{
89 console.log('[Click Listener] Detected click on {} button!');
90 window.sessionStorage.setItem('{}', 'true');
91 break;
92 }}
93 element = element.parentElement;
94 }}
95 }}, true);
96 }})()
97 "#,
98 button_text.replace('\'', "\\'"),
99 button_text.replace('\'', "\\'"),
100 button_text.replace('\'', "\\'"),
101 storage_key.replace('\'', "\\'")
102 );
103
104 match adapter.evaluate(&script).await {
105 Ok(_) => Ok(true),
106 Err(e) if is_navigation_error(&e.to_string()) => Ok(false),
107 Err(e) => Err(e),
108 }
109}
110
111pub async fn check_and_clear_flag(
122 adapter: &dyn EngineAdapter,
123 storage_key: &str,
124) -> Result<bool, EngineError> {
125 let script = format!(
126 r#"
127 (function() {{
128 const flag = window.sessionStorage.getItem('{}');
129 if (flag === 'true') {{
130 window.sessionStorage.removeItem('{}');
131 return true;
132 }}
133 return false;
134 }})()
135 "#,
136 storage_key.replace('\'', "\\'"),
137 storage_key.replace('\'', "\\'")
138 );
139
140 match adapter.evaluate(&script).await {
141 Ok(value) => Ok(value.as_bool().unwrap_or(false)),
142 Err(e) if is_navigation_error(&e.to_string()) => Ok(false),
143 Err(e) => Err(e),
144 }
145}
146
147pub async fn find_toggle_button(
162 adapter: &dyn EngineAdapter,
163 data_qa_selectors: &[&str],
164 text_to_find: Option<&str>,
165 element_types: &[&str],
166) -> Result<Option<String>, EngineError> {
167 for selector in data_qa_selectors {
169 let count = adapter.count(selector).await?;
170 if count > 0 {
171 return Ok(Some(selector.to_string()));
172 }
173 }
174
175 if let Some(text) = text_to_find {
177 for element_type in element_types {
178 let xpath = format!(
180 "//{}[contains(text(), '{}')]",
181 element_type,
182 text.replace('\'', "\\'")
183 );
184
185 let script = format!(
187 r#"
188 (function() {{
189 const result = document.evaluate('{}', document, null,
190 XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
191 return result.snapshotLength;
192 }})()
193 "#,
194 xpath.replace('\'', "\\'")
195 );
196
197 match adapter.evaluate(&script).await {
198 Ok(value) => {
199 if let Some(count) = value.as_u64() {
200 if count > 0 {
201 return Ok(Some(xpath));
202 }
203 }
204 }
205 Err(_) => continue,
206 }
207 }
208 }
209
210 Ok(None)
211}
212
213#[cfg(test)]
214mod tests {
215 #[allow(unused_imports)]
216 use super::*;
217
218 #[test]
219 fn install_click_listener_script_escapes_quotes() {
220 let _button_text = "Click me";
223 let _storage_key = "button_clicked";
224 }
225
226 #[test]
227 fn check_and_clear_flag_script_escapes_quotes() {
228 let _storage_key = "test_key";
230 }
231}