viewpoint_core/page/locator/actions/
mod.rs

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