viewpoint_core/page/locator/actions/
mod.rs

1//! Locator actions for element interaction.
2
3use tracing::{debug, instrument};
4use viewpoint_cdp::protocol::input::{
5    DispatchKeyEventParams, DispatchMouseEventParams, MouseButton,
6};
7
8use super::Locator;
9use super::builders::{ClickBuilder, HoverBuilder, TapBuilder, TypeBuilder};
10use crate::error::LocatorError;
11
12impl<'a> Locator<'a> {
13    /// Click the element.
14    ///
15    /// Returns a builder that can be configured with additional options, or awaited
16    /// directly for a simple click.
17    ///
18    /// # Examples
19    ///
20    /// ```no_run
21    /// use viewpoint_core::Page;
22    /// use viewpoint_cdp::protocol::input::{MouseButton, modifiers};
23    ///
24    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
25    /// // Simple click - await directly
26    /// page.locator("button").click().await?;
27    ///
28    /// // Click with options
29    /// page.locator("button").click()
30    ///     .position(10.0, 5.0)
31    ///     .button(MouseButton::Right)
32    ///     .modifiers(modifiers::SHIFT)
33    ///     .send().await?;
34    ///
35    /// // Force click without waiting for actionability
36    /// page.locator("button").click().force(true).await?;
37    /// # Ok(())
38    /// # }
39    /// ```
40    ///
41    /// # Options
42    ///
43    /// - [`ClickBuilder::position`] - Click at offset from element's top-left corner
44    /// - [`ClickBuilder::button`] - Use a different mouse button (right, middle)
45    /// - [`ClickBuilder::modifiers`] - Hold modifier keys (Shift, Ctrl, Alt)
46    /// - [`ClickBuilder::force`] - Skip actionability checks
47    pub fn click(&self) -> ClickBuilder<'_, 'a> {
48        ClickBuilder::new(self)
49    }
50
51    /// Double-click the element.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if the element cannot be clicked.
56    ///
57    /// # Panics
58    ///
59    /// Panics if a visible element lacks bounding box coordinates. This should
60    /// never occur as `wait_for_actionable` ensures visibility before returning.
61    #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
62    pub async fn dblclick(&self) -> Result<(), LocatorError> {
63        let info = self.wait_for_actionable().await?;
64
65        let x = info.x.expect("visible element has x")
66            + info.width.expect("visible element has width") / 2.0;
67        let y = info.y.expect("visible element has y")
68            + info.height.expect("visible element has height") / 2.0;
69
70        debug!(x, y, "Double-clicking element");
71
72        // First click
73        self.dispatch_mouse_event(DispatchMouseEventParams::mouse_move(x, y))
74            .await?;
75        self.dispatch_mouse_event(DispatchMouseEventParams::mouse_down(
76            x,
77            y,
78            MouseButton::Left,
79        ))
80        .await?;
81        self.dispatch_mouse_event(DispatchMouseEventParams::mouse_up(x, y, MouseButton::Left))
82            .await?;
83
84        // Second click
85        let mut down = DispatchMouseEventParams::mouse_down(x, y, MouseButton::Left);
86        down.click_count = Some(2);
87        self.dispatch_mouse_event(down).await?;
88
89        let mut up = DispatchMouseEventParams::mouse_up(x, y, MouseButton::Left);
90        up.click_count = Some(2);
91        self.dispatch_mouse_event(up).await?;
92
93        Ok(())
94    }
95
96    /// Fill the element with text (clears existing content first).
97    ///
98    /// This is for input and textarea elements.
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if the element cannot be focused or text cannot be inserted.
103    #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
104    pub async fn fill(&self, text: &str) -> Result<(), LocatorError> {
105        let _info = self.wait_for_actionable().await?;
106
107        debug!(text, "Filling element");
108
109        // Focus the element
110        self.focus_element().await?;
111
112        // Select all and delete (clear)
113        self.dispatch_key_event(DispatchKeyEventParams::key_down("a"))
114            .await?;
115        // Send Ctrl+A
116        let mut select_all = DispatchKeyEventParams::key_down("a");
117        select_all.modifiers = Some(viewpoint_cdp::protocol::input::modifiers::CTRL);
118        self.dispatch_key_event(select_all).await?;
119
120        // Delete selected text
121        self.dispatch_key_event(DispatchKeyEventParams::key_down("Backspace"))
122            .await?;
123
124        // Insert the new text
125        self.insert_text(text).await?;
126
127        Ok(())
128    }
129
130    /// Type text character by character.
131    ///
132    /// Unlike `fill`, this types each character with keydown/keyup events.
133    /// Returns a builder that can be configured with additional options, or awaited
134    /// directly for simple typing.
135    ///
136    /// # Examples
137    ///
138    /// ```no_run
139    /// use std::time::Duration;
140    /// use viewpoint_core::Page;
141    ///
142    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
143    /// // Simple type - await directly
144    /// page.locator("input").type_text("hello").await?;
145    ///
146    /// // Type with delay between characters
147    /// page.locator("input").type_text("hello")
148    ///     .delay(Duration::from_millis(100))
149    ///     .send().await?;
150    /// # Ok(())
151    /// # }
152    /// ```
153    ///
154    /// # Options
155    ///
156    /// - [`TypeBuilder::delay`] - Add delay between character keystrokes
157    pub fn type_text(&self, text: &str) -> TypeBuilder<'_, 'a> {
158        TypeBuilder::new(self, text)
159    }
160
161    /// Press a key or key combination.
162    ///
163    /// Examples: "Enter", "Backspace", "Control+a", "Shift+Tab"
164    ///
165    /// # Errors
166    ///
167    /// Returns an error if the element cannot be focused or the key cannot be pressed.
168    #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
169    pub async fn press(&self, key: &str) -> Result<(), LocatorError> {
170        self.wait_for_actionable().await?;
171
172        debug!(key, "Pressing key");
173
174        // Focus the element
175        self.focus_element().await?;
176
177        // Parse modifiers and key
178        let parts: Vec<&str> = key.split('+').collect();
179        let actual_key = parts.last().unwrap_or(&key);
180
181        let mut modifiers = 0;
182        for part in &parts[..parts.len().saturating_sub(1)] {
183            match part.to_lowercase().as_str() {
184                "control" | "ctrl" => {
185                    modifiers |= viewpoint_cdp::protocol::input::modifiers::CTRL;
186                }
187                "alt" => modifiers |= viewpoint_cdp::protocol::input::modifiers::ALT,
188                "shift" => modifiers |= viewpoint_cdp::protocol::input::modifiers::SHIFT,
189                "meta" | "cmd" => modifiers |= viewpoint_cdp::protocol::input::modifiers::META,
190                _ => {}
191            }
192        }
193
194        // Key down
195        let mut key_down = DispatchKeyEventParams::key_down(actual_key);
196        if modifiers != 0 {
197            key_down.modifiers = Some(modifiers);
198        }
199        self.dispatch_key_event(key_down).await?;
200
201        // Key up
202        let mut key_up = DispatchKeyEventParams::key_up(actual_key);
203        if modifiers != 0 {
204            key_up.modifiers = Some(modifiers);
205        }
206        self.dispatch_key_event(key_up).await?;
207
208        Ok(())
209    }
210
211    /// Hover over the element.
212    ///
213    /// Returns a builder that can be configured with additional options, or awaited
214    /// directly for a simple hover.
215    ///
216    /// # Examples
217    ///
218    /// ```no_run
219    /// use viewpoint_core::Page;
220    ///
221    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
222    /// // Simple hover - await directly
223    /// page.locator("button").hover().await?;
224    ///
225    /// // Hover with position offset
226    /// page.locator("button").hover()
227    ///     .position(10.0, 5.0)
228    ///     .send().await?;
229    ///
230    /// // Force hover without waiting for actionability
231    /// page.locator("button").hover().force(true).await?;
232    /// # Ok(())
233    /// # }
234    /// ```
235    ///
236    /// # Options
237    ///
238    /// - [`HoverBuilder::position`] - Hover at offset from element's top-left corner
239    /// - [`HoverBuilder::modifiers`] - Hold modifier keys during hover
240    /// - [`HoverBuilder::force`] - Skip actionability checks
241    pub fn hover(&self) -> HoverBuilder<'_, 'a> {
242        HoverBuilder::new(self)
243    }
244
245    /// Focus the element.
246    ///
247    /// # Errors
248    ///
249    /// Returns an error if the element cannot be found or focused.
250    #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
251    pub async fn focus(&self) -> Result<(), LocatorError> {
252        self.wait_for_actionable().await?;
253
254        debug!("Focusing element");
255        self.focus_element().await?;
256
257        Ok(())
258    }
259
260    /// Clear the element's content.
261    ///
262    /// # Errors
263    ///
264    /// Returns an error if the element cannot be cleared.
265    #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
266    pub async fn clear(&self) -> Result<(), LocatorError> {
267        self.wait_for_actionable().await?;
268
269        debug!("Clearing element");
270
271        // Focus and select all, then delete
272        self.focus_element().await?;
273
274        let mut select_all = DispatchKeyEventParams::key_down("a");
275        select_all.modifiers = Some(viewpoint_cdp::protocol::input::modifiers::CTRL);
276        self.dispatch_key_event(select_all).await?;
277
278        self.dispatch_key_event(DispatchKeyEventParams::key_down("Backspace"))
279            .await?;
280
281        Ok(())
282    }
283
284    /// Check a checkbox or radio button.
285    ///
286    /// # Errors
287    ///
288    /// Returns an error if the element cannot be checked.
289    #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
290    pub async fn check(&self) -> Result<(), LocatorError> {
291        let is_checked = self.is_checked().await?;
292
293        if is_checked {
294            debug!("Element already checked");
295        } else {
296            debug!("Checking element");
297            self.click().await?;
298        }
299
300        Ok(())
301    }
302
303    /// Uncheck a checkbox.
304    ///
305    /// # Errors
306    ///
307    /// Returns an error if the element cannot be unchecked.
308    #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
309    pub async fn uncheck(&self) -> Result<(), LocatorError> {
310        let is_checked = self.is_checked().await?;
311
312        if is_checked {
313            debug!("Unchecking element");
314            self.click().await?;
315        } else {
316            debug!("Element already unchecked");
317        }
318
319        Ok(())
320    }
321
322    /// Tap on the element (touch event).
323    ///
324    /// Requires touch to be enabled via `page.touchscreen().enable()`.
325    ///
326    /// Returns a builder to configure tap options.
327    ///
328    /// # Example
329    ///
330    /// ```no_run
331    /// use viewpoint_core::Page;
332    /// use viewpoint_cdp::protocol::input::modifiers;
333    ///
334    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
335    /// // Simple tap
336    /// page.locator("button").tap().send().await?;
337    ///
338    /// // Tap with position offset
339    /// page.locator("button").tap().position(10.0, 5.0).send().await?;
340    ///
341    /// // Tap with modifiers
342    /// page.locator("button").tap().modifiers(modifiers::SHIFT).send().await?;
343    ///
344    /// // Force tap without waiting for actionability
345    /// page.locator("button").tap().force(true).send().await?;
346    /// # Ok(())
347    /// # }
348    /// ```
349    pub fn tap(&self) -> TapBuilder<'_, 'a> {
350        TapBuilder::new(self)
351    }
352
353    /// Drag this element to another locator.
354    ///
355    /// # Arguments
356    ///
357    /// * `target` - The target locator to drag to.
358    ///
359    /// # Example
360    ///
361    /// ```no_run
362    /// use viewpoint_core::Page;
363    ///
364    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
365    /// let source = page.locator("#draggable");
366    /// let target = page.locator("#droppable");
367    /// source.drag_to(&target).await?;
368    /// # Ok(())
369    /// # }
370    /// ```
371    #[instrument(level = "debug", skip(self, target), fields(selector = ?self.selector))]
372    pub async fn drag_to(&self, target: &Locator<'_>) -> Result<(), LocatorError> {
373        self.drag_to_with_options(target, None, None, 1).await
374    }
375
376    /// Drag this element to another locator with options.
377    ///
378    /// # Arguments
379    ///
380    /// * `target` - The target locator to drag to.
381    /// * `source_position` - Optional offset from source element's top-left corner.
382    /// * `target_position` - Optional offset from target element's top-left corner.
383    /// * `steps` - Number of intermediate steps for smooth dragging.
384    #[instrument(level = "debug", skip(self, target))]
385    pub async fn drag_to_with_options(
386        &self,
387        target: &Locator<'_>,
388        source_position: Option<(f64, f64)>,
389        target_position: Option<(f64, f64)>,
390        steps: u32,
391    ) -> Result<(), LocatorError> {
392        // Get source element info
393        let source_info = self.wait_for_actionable().await?;
394        let (source_x, source_y) = if let Some((ox, oy)) = source_position {
395            (
396                source_info.x.expect("x") + ox,
397                source_info.y.expect("y") + oy,
398            )
399        } else {
400            (
401                source_info.x.expect("x") + source_info.width.expect("width") / 2.0,
402                source_info.y.expect("y") + source_info.height.expect("height") / 2.0,
403            )
404        };
405
406        // Get target element info
407        let target_info = target.wait_for_actionable().await?;
408        let (target_x, target_y) = if let Some((ox, oy)) = target_position {
409            (
410                target_info.x.expect("x") + ox,
411                target_info.y.expect("y") + oy,
412            )
413        } else {
414            (
415                target_info.x.expect("x") + target_info.width.expect("width") / 2.0,
416                target_info.y.expect("y") + target_info.height.expect("height") / 2.0,
417            )
418        };
419
420        debug!(
421            "Dragging from ({}, {}) to ({}, {})",
422            source_x, source_y, target_x, target_y
423        );
424
425        // Perform drag operation
426        self.page.mouse().move_(source_x, source_y).send().await?;
427        self.page.mouse().down().send().await?;
428        self.page
429            .mouse()
430            .move_(target_x, target_y)
431            .steps(steps)
432            .send()
433            .await?;
434        self.page.mouse().up().send().await?;
435
436        Ok(())
437    }
438
439    /// Take a screenshot of this element.
440    ///
441    /// Returns a builder to configure screenshot options.
442    ///
443    /// # Example
444    ///
445    /// ```no_run
446    /// use viewpoint_core::Page;
447    ///
448    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
449    /// // Capture element screenshot
450    /// let bytes = page.locator("button").screenshot().capture().await?;
451    ///
452    /// // Capture and save to file
453    /// page.locator("button")
454    ///     .screenshot()
455    ///     .path("button.png")
456    ///     .capture()
457    ///     .await?;
458    /// # Ok(())
459    /// # }
460    /// ```
461    pub fn screenshot(&self) -> crate::page::screenshot_element::ElementScreenshotBuilder<'_, '_> {
462        crate::page::screenshot_element::ElementScreenshotBuilder::new(self)
463    }
464}