viewpoint_core/page/locator/actions/
mod.rs

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