1use crate::tools::browser::executor::BrowserError;
4use crate::tools::browser::types::{BoundingBox, SnapshotResult};
5use serde_json::Value as JsonValue;
6use std::collections::HashMap;
7
8pub trait QueryExt {
10 fn snapshot(
12 &self,
13 ) -> impl std::future::Future<Output = Result<SnapshotResult, BrowserError>> + Send;
14
15 fn screenshot(
17 &self,
18 path: &str,
19 ) -> impl std::future::Future<Output = Result<(), BrowserError>> + Send;
20
21 fn pdf(&self, path: &str)
23 -> impl std::future::Future<Output = Result<(), BrowserError>> + Send;
24
25 fn eval(
27 &self,
28 script: &str,
29 ) -> impl std::future::Future<Output = Result<JsonValue, BrowserError>> + Send;
30
31 fn get(
33 &self,
34 what: &str,
35 ) -> impl std::future::Future<Output = Result<String, BrowserError>> + Send;
36
37 fn get_attr(
39 &self,
40 selector: &str,
41 attr: &str,
42 ) -> impl std::future::Future<Output = Result<String, BrowserError>> + Send;
43
44 fn get_count(
46 &self,
47 selector: &str,
48 ) -> impl std::future::Future<Output = Result<usize, BrowserError>> + Send;
49
50 fn get_box(
52 &self,
53 selector: &str,
54 ) -> impl std::future::Future<Output = Result<BoundingBox, BrowserError>> + Send;
55
56 fn get_styles(
58 &self,
59 selector: &str,
60 ) -> impl std::future::Future<Output = Result<HashMap<String, String>, BrowserError>> + Send;
61
62 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 fn is_(
73 &self,
74 what: &str,
75 selector: &str,
76 ) -> impl std::future::Future<Output = Result<bool, BrowserError>> + Send;
77
78 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}