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