Skip to main content

limit_cli/tools/browser/client_ext/
interaction.rs

1//! Interaction operations
2
3use crate::tools::browser::executor::BrowserError;
4
5/// Interaction operations for browser client
6pub trait InteractionExt {
7    /// Click an element by selector
8    fn click(
9        &self,
10        selector: &str,
11    ) -> impl std::future::Future<Output = Result<(), BrowserError>> + Send;
12
13    /// Double-click an element
14    fn dblclick(
15        &self,
16        selector: &str,
17    ) -> impl std::future::Future<Output = Result<(), BrowserError>> + Send;
18
19    /// Fill a form field with text
20    fn fill(
21        &self,
22        selector: &str,
23        text: &str,
24    ) -> impl std::future::Future<Output = Result<(), BrowserError>> + Send;
25
26    /// Type text into an element (character by character)
27    fn type_text(
28        &self,
29        selector: &str,
30        text: &str,
31    ) -> impl std::future::Future<Output = Result<(), BrowserError>> + Send;
32
33    /// Press a keyboard key
34    fn press(
35        &self,
36        key: &str,
37    ) -> impl std::future::Future<Output = Result<(), BrowserError>> + Send;
38
39    /// Hover over an element
40    fn hover(
41        &self,
42        selector: &str,
43    ) -> impl std::future::Future<Output = Result<(), BrowserError>> + Send;
44
45    /// Select an option in a dropdown
46    fn select_option(
47        &self,
48        selector: &str,
49        value: &str,
50    ) -> impl std::future::Future<Output = Result<(), BrowserError>> + Send;
51
52    /// Focus an element
53    fn focus(
54        &self,
55        selector: &str,
56    ) -> impl std::future::Future<Output = Result<(), BrowserError>> + Send;
57
58    /// Check a checkbox
59    fn check(
60        &self,
61        selector: &str,
62    ) -> impl std::future::Future<Output = Result<(), BrowserError>> + Send;
63
64    /// Uncheck a checkbox
65    fn uncheck(
66        &self,
67        selector: &str,
68    ) -> impl std::future::Future<Output = Result<(), BrowserError>> + Send;
69
70    /// Scroll an element into view
71    fn scrollintoview(
72        &self,
73        selector: &str,
74    ) -> impl std::future::Future<Output = Result<(), BrowserError>> + Send;
75
76    /// Drag and drop from source to destination
77    fn drag(
78        &self,
79        source: &str,
80        target: &str,
81    ) -> impl std::future::Future<Output = Result<(), BrowserError>> + Send;
82
83    /// Upload files to a file input
84    fn upload(
85        &self,
86        selector: &str,
87        files: &[&str],
88    ) -> impl std::future::Future<Output = Result<(), BrowserError>> + Send;
89
90    /// Scroll the page
91    fn scroll(
92        &self,
93        direction: &str,
94        pixels: Option<u32>,
95    ) -> impl std::future::Future<Output = Result<(), BrowserError>> + Send;
96}
97
98impl InteractionExt for super::super::BrowserClient {
99    async fn click(&self, selector: &str) -> Result<(), BrowserError> {
100        if selector.is_empty() {
101            return Err(BrowserError::InvalidArguments(
102                "Selector cannot be empty".to_string(),
103            ));
104        }
105
106        let output = self.executor().execute(&["click", selector]).await?;
107
108        if output.success {
109            Ok(())
110        } else {
111            Err(BrowserError::Other(format!(
112                "Failed to click element: {}",
113                output.stderr
114            )))
115        }
116    }
117
118    async fn dblclick(&self, selector: &str) -> Result<(), BrowserError> {
119        if selector.is_empty() {
120            return Err(BrowserError::InvalidArguments(
121                "Selector cannot be empty".to_string(),
122            ));
123        }
124
125        let output = self.executor().execute(&["dblclick", selector]).await?;
126
127        if output.success {
128            Ok(())
129        } else {
130            Err(BrowserError::Other(format!(
131                "Failed to double-click element: {}",
132                output.stderr
133            )))
134        }
135    }
136
137    async fn fill(&self, selector: &str, text: &str) -> Result<(), BrowserError> {
138        if selector.is_empty() {
139            return Err(BrowserError::InvalidArguments(
140                "Selector cannot be empty".to_string(),
141            ));
142        }
143
144        let output = self.executor().execute(&["fill", selector, text]).await?;
145
146        if output.success {
147            Ok(())
148        } else {
149            Err(BrowserError::Other(format!(
150                "Failed to fill element: {}",
151                output.stderr
152            )))
153        }
154    }
155
156    async fn type_text(&self, selector: &str, text: &str) -> Result<(), BrowserError> {
157        if selector.is_empty() {
158            return Err(BrowserError::InvalidArguments(
159                "Selector cannot be empty".to_string(),
160            ));
161        }
162
163        let output = self.executor().execute(&["type", selector, text]).await?;
164
165        if output.success {
166            Ok(())
167        } else {
168            Err(BrowserError::Other(format!(
169                "Failed to type text: {}",
170                output.stderr
171            )))
172        }
173    }
174
175    async fn press(&self, key: &str) -> Result<(), BrowserError> {
176        if key.is_empty() {
177            return Err(BrowserError::InvalidArguments(
178                "Key cannot be empty".to_string(),
179            ));
180        }
181
182        let output = self.executor().execute(&["press", key]).await?;
183
184        if output.success {
185            Ok(())
186        } else {
187            Err(BrowserError::Other(format!(
188                "Failed to press key: {}",
189                output.stderr
190            )))
191        }
192    }
193
194    async fn hover(&self, selector: &str) -> Result<(), BrowserError> {
195        if selector.is_empty() {
196            return Err(BrowserError::InvalidArguments(
197                "Selector cannot be empty".to_string(),
198            ));
199        }
200
201        let output = self.executor().execute(&["hover", selector]).await?;
202
203        if output.success {
204            Ok(())
205        } else {
206            Err(BrowserError::Other(format!(
207                "Failed to hover: {}",
208                output.stderr
209            )))
210        }
211    }
212
213    async fn select_option(&self, selector: &str, value: &str) -> Result<(), BrowserError> {
214        if selector.is_empty() {
215            return Err(BrowserError::InvalidArguments(
216                "Selector cannot be empty".to_string(),
217            ));
218        }
219
220        let output = self
221            .executor()
222            .execute(&["select", selector, value])
223            .await?;
224
225        if output.success {
226            Ok(())
227        } else {
228            Err(BrowserError::Other(format!(
229                "Failed to select option: {}",
230                output.stderr
231            )))
232        }
233    }
234
235    async fn focus(&self, selector: &str) -> Result<(), BrowserError> {
236        if selector.is_empty() {
237            return Err(BrowserError::InvalidArguments(
238                "Selector cannot be empty".to_string(),
239            ));
240        }
241
242        let output = self.executor().execute(&["focus", selector]).await?;
243
244        if output.success {
245            Ok(())
246        } else {
247            Err(BrowserError::Other(format!(
248                "Failed to focus element: {}",
249                output.stderr
250            )))
251        }
252    }
253
254    async fn check(&self, selector: &str) -> Result<(), BrowserError> {
255        if selector.is_empty() {
256            return Err(BrowserError::InvalidArguments(
257                "Selector cannot be empty".to_string(),
258            ));
259        }
260
261        let output = self.executor().execute(&["check", selector]).await?;
262
263        if output.success {
264            Ok(())
265        } else {
266            Err(BrowserError::Other(format!(
267                "Failed to check element: {}",
268                output.stderr
269            )))
270        }
271    }
272
273    async fn uncheck(&self, selector: &str) -> Result<(), BrowserError> {
274        if selector.is_empty() {
275            return Err(BrowserError::InvalidArguments(
276                "Selector cannot be empty".to_string(),
277            ));
278        }
279
280        let output = self.executor().execute(&["uncheck", selector]).await?;
281
282        if output.success {
283            Ok(())
284        } else {
285            Err(BrowserError::Other(format!(
286                "Failed to uncheck element: {}",
287                output.stderr
288            )))
289        }
290    }
291
292    async fn scrollintoview(&self, selector: &str) -> Result<(), BrowserError> {
293        if selector.is_empty() {
294            return Err(BrowserError::InvalidArguments(
295                "Selector cannot be empty".to_string(),
296            ));
297        }
298
299        let output = self
300            .executor()
301            .execute(&["scrollintoview", selector])
302            .await?;
303
304        if output.success {
305            Ok(())
306        } else {
307            Err(BrowserError::Other(format!(
308                "Failed to scroll element into view: {}",
309                output.stderr
310            )))
311        }
312    }
313
314    async fn drag(&self, source: &str, target: &str) -> Result<(), BrowserError> {
315        if source.is_empty() || target.is_empty() {
316            return Err(BrowserError::InvalidArguments(
317                "Source and target selectors cannot be empty".to_string(),
318            ));
319        }
320
321        let output = self.executor().execute(&["drag", source, target]).await?;
322
323        if output.success {
324            Ok(())
325        } else {
326            Err(BrowserError::Other(format!(
327                "Failed to drag and drop: {}",
328                output.stderr
329            )))
330        }
331    }
332
333    async fn upload(&self, selector: &str, files: &[&str]) -> Result<(), BrowserError> {
334        if selector.is_empty() {
335            return Err(BrowserError::InvalidArguments(
336                "Selector cannot be empty".to_string(),
337            ));
338        }
339
340        if files.is_empty() {
341            return Err(BrowserError::InvalidArguments(
342                "At least one file must be specified".to_string(),
343            ));
344        }
345
346        let mut args = vec!["upload", selector];
347        let files_owned: Vec<String> = files.iter().map(|f| f.to_string()).collect();
348        let file_refs: Vec<&str> = files_owned.iter().map(|f| f.as_str()).collect();
349        args.extend(file_refs);
350
351        let output = self.executor().execute(&args).await?;
352
353        if output.success {
354            Ok(())
355        } else {
356            Err(BrowserError::Other(format!(
357                "Failed to upload files: {}",
358                output.stderr
359            )))
360        }
361    }
362
363    async fn scroll(&self, direction: &str, pixels: Option<u32>) -> Result<(), BrowserError> {
364        let valid_directions = ["up", "down", "left", "right"];
365        if !valid_directions.contains(&direction) {
366            return Err(BrowserError::InvalidArguments(format!(
367                "Invalid scroll direction '{}'. Valid directions: {}",
368                direction,
369                valid_directions.join(", ")
370            )));
371        }
372
373        let output = match pixels {
374            Some(px) => {
375                let px_str = px.to_string();
376                self.executor()
377                    .execute(&["scroll", direction, &px_str])
378                    .await?
379            }
380            None => self.executor().execute(&["scroll", direction]).await?,
381        };
382
383        if output.success {
384            Ok(())
385        } else {
386            Err(BrowserError::Other(format!(
387                "Failed to scroll: {}",
388                output.stderr
389            )))
390        }
391    }
392}