Skip to main content

limit_cli/tools/browser/client_ext/
query.rs

1//! Query and snapshot operations
2
3use crate::tools::browser::executor::BrowserError;
4use crate::tools::browser::types::{BoundingBox, SnapshotResult};
5use serde_json::Value as JsonValue;
6use std::collections::HashMap;
7
8/// Query and snapshot operations for browser client
9pub trait QueryExt {
10    /// Take an accessibility snapshot of the current page
11    fn snapshot(
12        &self,
13    ) -> impl std::future::Future<Output = Result<SnapshotResult, BrowserError>> + Send;
14
15    /// Take a screenshot and save to path
16    fn screenshot(
17        &self,
18        path: &str,
19    ) -> impl std::future::Future<Output = Result<(), BrowserError>> + Send;
20
21    /// Save page as PDF
22    fn pdf(&self, path: &str)
23        -> impl std::future::Future<Output = Result<(), BrowserError>> + Send;
24
25    /// Evaluate JavaScript in the browser
26    fn eval(
27        &self,
28        script: &str,
29    ) -> impl std::future::Future<Output = Result<JsonValue, BrowserError>> + Send;
30
31    /// Get page content (text, html, value, url, or title)
32    fn get(
33        &self,
34        what: &str,
35    ) -> impl std::future::Future<Output = Result<String, BrowserError>> + Send;
36
37    /// Get element attribute value
38    fn get_attr(
39        &self,
40        selector: &str,
41        attr: &str,
42    ) -> impl std::future::Future<Output = Result<String, BrowserError>> + Send;
43
44    /// Get count of elements matching selector
45    fn get_count(
46        &self,
47        selector: &str,
48    ) -> impl std::future::Future<Output = Result<usize, BrowserError>> + Send;
49
50    /// Get element bounding box
51    fn get_box(
52        &self,
53        selector: &str,
54    ) -> impl std::future::Future<Output = Result<BoundingBox, BrowserError>> + Send;
55
56    /// Get element computed styles
57    fn get_styles(
58        &self,
59        selector: &str,
60    ) -> impl std::future::Future<Output = Result<HashMap<String, String>, BrowserError>> + Send;
61
62    /// Find elements using various locator strategies
63    fn find(
64        &self,
65        locator_type: &str,
66        value: &str,
67        action: &str,
68        action_value: Option<&str>,
69    ) -> impl std::future::Future<Output = Result<String, BrowserError>> + Send;
70
71    /// Check element state (visible, hidden, enabled, disabled, editable)
72    fn is_(
73        &self,
74        what: &str,
75        selector: &str,
76    ) -> impl std::future::Future<Output = Result<bool, BrowserError>> + Send;
77
78    /// Download file from link/button to path
79    fn download(
80        &self,
81        selector: &str,
82        path: &str,
83    ) -> impl std::future::Future<Output = Result<String, BrowserError>> + Send;
84}
85
86impl QueryExt for super::super::BrowserClient {
87    async fn snapshot(&self) -> Result<SnapshotResult, BrowserError> {
88        let output = self.executor().execute(&["snapshot"]).await?;
89
90        if output.success {
91            let content = output.stdout.trim().to_string();
92            let title = Self::extract_field(&content, "Title:");
93            let url = Self::extract_field(&content, "URL:");
94
95            Ok(SnapshotResult {
96                content,
97                title,
98                url,
99            })
100        } else {
101            Err(BrowserError::Other(format!(
102                "Failed to take snapshot: {}",
103                output.stderr
104            )))
105        }
106    }
107
108    async fn screenshot(&self, path: &str) -> Result<(), BrowserError> {
109        if path.is_empty() {
110            return Err(BrowserError::InvalidArguments(
111                "Path cannot be empty".to_string(),
112            ));
113        }
114
115        let output = self.executor().execute(&["screenshot", path]).await?;
116
117        if output.success {
118            Ok(())
119        } else {
120            Err(BrowserError::Other(format!(
121                "Failed to take screenshot: {}",
122                output.stderr
123            )))
124        }
125    }
126
127    async fn pdf(&self, path: &str) -> Result<(), BrowserError> {
128        if path.is_empty() {
129            return Err(BrowserError::InvalidArguments(
130                "Path cannot be empty".to_string(),
131            ));
132        }
133
134        let output = self.executor().execute(&["pdf", path]).await?;
135
136        if output.success {
137            Ok(())
138        } else {
139            Err(BrowserError::Other(format!(
140                "Failed to save PDF: {}",
141                output.stderr
142            )))
143        }
144    }
145
146    async fn eval(&self, script: &str) -> Result<JsonValue, BrowserError> {
147        if script.is_empty() {
148            return Err(BrowserError::InvalidArguments(
149                "Script cannot be empty".to_string(),
150            ));
151        }
152
153        let output = self.executor().execute(&["eval", script]).await?;
154
155        if output.success {
156            let trimmed = output.stdout.trim();
157            if trimmed.is_empty() {
158                Ok(JsonValue::Null)
159            } else {
160                serde_json::from_str(trimmed)
161                    .map_err(|e| BrowserError::ParseError(format!("Invalid JSON: {}", e)))
162            }
163        } else {
164            Err(BrowserError::Other(format!(
165                "Failed to evaluate script: {}",
166                output.stderr
167            )))
168        }
169    }
170
171    async fn get(&self, what: &str) -> Result<String, BrowserError> {
172        let valid_types = ["text", "html", "value", "url", "title"];
173        if !valid_types.contains(&what) {
174            return Err(BrowserError::InvalidArguments(format!(
175                "Invalid get type '{}'. Valid types: {}",
176                what,
177                valid_types.join(", ")
178            )));
179        }
180
181        let output = self.executor().execute(&["get", what]).await?;
182
183        if output.success {
184            Ok(output.stdout.trim().to_string())
185        } else {
186            Err(BrowserError::Other(format!(
187                "Failed to get {}: {}",
188                what, output.stderr
189            )))
190        }
191    }
192
193    async fn get_attr(&self, selector: &str, attr: &str) -> Result<String, BrowserError> {
194        if selector.is_empty() {
195            return Err(BrowserError::InvalidArguments(
196                "Selector cannot be empty".to_string(),
197            ));
198        }
199
200        if attr.is_empty() {
201            return Err(BrowserError::InvalidArguments(
202                "Attribute name cannot be empty".to_string(),
203            ));
204        }
205
206        let output = self
207            .executor()
208            .execute(&["get", "attr", selector, attr])
209            .await?;
210
211        if output.success {
212            Ok(output.stdout.trim().to_string())
213        } else {
214            Err(BrowserError::Other(format!(
215                "Failed to get attribute: {}",
216                output.stderr
217            )))
218        }
219    }
220
221    async fn get_count(&self, selector: &str) -> Result<usize, BrowserError> {
222        if selector.is_empty() {
223            return Err(BrowserError::InvalidArguments(
224                "Selector cannot be empty".to_string(),
225            ));
226        }
227
228        let output = self.executor().execute(&["get", "count", selector]).await?;
229
230        if output.success {
231            let count = output
232                .stdout
233                .trim()
234                .parse::<usize>()
235                .map_err(|_| BrowserError::ParseError("Invalid count value".to_string()))?;
236            Ok(count)
237        } else {
238            Err(BrowserError::Other(format!(
239                "Failed to get count: {}",
240                output.stderr
241            )))
242        }
243    }
244
245    async fn get_box(&self, selector: &str) -> Result<BoundingBox, BrowserError> {
246        if selector.is_empty() {
247            return Err(BrowserError::InvalidArguments(
248                "Selector cannot be empty".to_string(),
249            ));
250        }
251
252        let output = self.executor().execute(&["get", "box", selector]).await?;
253
254        if output.success {
255            let parts: Vec<&str> = output.stdout.trim().split(',').collect();
256            if parts.len() == 4 {
257                Ok(BoundingBox {
258                    x: parts[0]
259                        .parse()
260                        .map_err(|_| BrowserError::ParseError("Invalid x value".to_string()))?,
261                    y: parts[1]
262                        .parse()
263                        .map_err(|_| BrowserError::ParseError("Invalid y value".to_string()))?,
264                    width: parts[2]
265                        .parse()
266                        .map_err(|_| BrowserError::ParseError("Invalid width value".to_string()))?,
267                    height: parts[3].parse().map_err(|_| {
268                        BrowserError::ParseError("Invalid height value".to_string())
269                    })?,
270                })
271            } else {
272                Err(BrowserError::ParseError(
273                    "Invalid bounding box format".to_string(),
274                ))
275            }
276        } else {
277            Err(BrowserError::Other(format!(
278                "Failed to get bounding box: {}",
279                output.stderr
280            )))
281        }
282    }
283
284    async fn get_styles(&self, selector: &str) -> Result<HashMap<String, String>, BrowserError> {
285        if selector.is_empty() {
286            return Err(BrowserError::InvalidArguments(
287                "Selector cannot be empty".to_string(),
288            ));
289        }
290
291        let output = self
292            .executor()
293            .execute(&["get", "styles", selector])
294            .await?;
295
296        if output.success {
297            let mut styles = HashMap::new();
298            for line in output.stdout.lines() {
299                if let Some((key, value)) = line.split_once(':') {
300                    styles.insert(key.trim().to_string(), value.trim().to_string());
301                }
302            }
303            Ok(styles)
304        } else {
305            Err(BrowserError::Other(format!(
306                "Failed to get styles: {}",
307                output.stderr
308            )))
309        }
310    }
311
312    async fn find(
313        &self,
314        locator_type: &str,
315        value: &str,
316        action: &str,
317        action_value: Option<&str>,
318    ) -> Result<String, BrowserError> {
319        let valid_locators = [
320            "role",
321            "text",
322            "label",
323            "placeholder",
324            "alt",
325            "title",
326            "testid",
327            "css",
328            "xpath",
329        ];
330        if !valid_locators.contains(&locator_type) {
331            return Err(BrowserError::InvalidArguments(format!(
332                "Invalid locator type '{}'. Valid types: {}",
333                locator_type,
334                valid_locators.join(", ")
335            )));
336        }
337
338        if value.is_empty() {
339            return Err(BrowserError::InvalidArguments(
340                "Locator value cannot be empty".to_string(),
341            ));
342        }
343
344        let valid_actions = [
345            "click", "fill", "text", "count", "first", "last", "nth", "hover", "focus", "check",
346            "uncheck",
347        ];
348        if !valid_actions.contains(&action) {
349            return Err(BrowserError::InvalidArguments(format!(
350                "Invalid action '{}'. Valid actions: {}",
351                action,
352                valid_actions.join(", ")
353            )));
354        }
355
356        let locator_flag = format!("--{}", locator_type);
357        let mut args = vec!["find", &locator_flag, value, action];
358
359        let output = if let Some(av) = action_value {
360            args.push(av);
361            self.executor().execute(&args).await?
362        } else {
363            self.executor().execute(&args).await?
364        };
365
366        if output.success {
367            Ok(output.stdout.trim().to_string())
368        } else {
369            Err(BrowserError::Other(format!(
370                "Find action failed: {}",
371                output.stderr
372            )))
373        }
374    }
375
376    async fn is_(&self, what: &str, selector: &str) -> Result<bool, BrowserError> {
377        let valid_states = ["visible", "hidden", "enabled", "disabled", "editable"];
378        if !valid_states.contains(&what) {
379            return Err(BrowserError::InvalidArguments(format!(
380                "Invalid state check '{}'. Valid states: {}",
381                what,
382                valid_states.join(", ")
383            )));
384        }
385
386        if selector.is_empty() {
387            return Err(BrowserError::InvalidArguments(
388                "Selector cannot be empty".to_string(),
389            ));
390        }
391
392        let output = self.executor().execute(&["is", what, selector]).await?;
393
394        if output.success {
395            let result = output.stdout.trim().to_lowercase();
396            Ok(result == "true" || result == "yes" || result == "1")
397        } else {
398            Err(BrowserError::Other(format!(
399                "Failed to check state: {}",
400                output.stderr
401            )))
402        }
403    }
404
405    async fn download(&self, selector: &str, path: &str) -> Result<String, BrowserError> {
406        if selector.is_empty() {
407            return Err(BrowserError::InvalidArguments(
408                "Selector cannot be empty".to_string(),
409            ));
410        }
411
412        let output = self
413            .executor()
414            .execute(&["download", selector, path])
415            .await?;
416
417        if output.success {
418            Ok(output.stdout.trim().to_string())
419        } else {
420            Err(BrowserError::Other(format!(
421                "Failed to download: {}",
422                output.stderr
423            )))
424        }
425    }
426}