viewpoint_core/page/locator/builders/
mod.rs

1//! Builder types for locator actions.
2//!
3//! These builders provide a fluent API for configuring locator operations
4//! like click, type, hover, and tap with various options.
5
6use std::time::Duration;
7
8use viewpoint_cdp::protocol::input::{DispatchKeyEventParams, DispatchMouseEventParams, MouseButton};
9use tracing::{debug, instrument};
10
11use super::Locator;
12use crate::error::LocatorError;
13
14// =============================================================================
15// ClickBuilder
16// =============================================================================
17
18/// Builder for click operations with configurable options.
19///
20/// Created via [`Locator::click`].
21///
22/// # Example
23///
24/// ```ignore
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/// ```
38#[derive(Debug)]
39pub struct ClickBuilder<'l, 'a> {
40    locator: &'l Locator<'a>,
41    /// Position offset from element's top-left corner.
42    position: Option<(f64, f64)>,
43    /// Mouse button to use.
44    button: MouseButton,
45    /// Modifier keys to hold during the click.
46    modifiers: i32,
47    /// Whether to bypass actionability checks.
48    force: bool,
49    /// Click count (1 for single click, 2 for double click).
50    click_count: i32,
51}
52
53impl<'l, 'a> ClickBuilder<'l, 'a> {
54    pub(crate) fn new(locator: &'l Locator<'a>) -> Self {
55        Self {
56            locator,
57            position: None,
58            button: MouseButton::Left,
59            modifiers: 0,
60            force: false,
61            click_count: 1,
62        }
63    }
64
65    /// Set the position offset from the element's top-left corner.
66    ///
67    /// By default, clicks the center of the element.
68    #[must_use]
69    pub fn position(mut self, x: f64, y: f64) -> Self {
70        self.position = Some((x, y));
71        self
72    }
73
74    /// Set the mouse button to use.
75    #[must_use]
76    pub fn button(mut self, button: MouseButton) -> Self {
77        self.button = button;
78        self
79    }
80
81    /// Set modifier keys to hold during the click.
82    ///
83    /// Use the `modifiers` constants from `viewpoint_cdp::protocol::input::modifiers`.
84    #[must_use]
85    pub fn modifiers(mut self, modifiers: i32) -> Self {
86        self.modifiers = modifiers;
87        self
88    }
89
90    /// Whether to bypass actionability checks.
91    ///
92    /// When `true`, the click will be performed immediately without waiting
93    /// for the element to be visible, enabled, or stable.
94    #[must_use]
95    pub fn force(mut self, force: bool) -> Self {
96        self.force = force;
97        self
98    }
99
100    /// Set the click count (internal use for double-click support).
101    #[must_use]
102    pub(crate) fn click_count(mut self, count: i32) -> Self {
103        self.click_count = count;
104        self
105    }
106
107    /// Execute the click operation.
108    #[instrument(level = "debug", skip(self), fields(selector = ?self.locator.selector))]
109    pub async fn send(self) -> Result<(), LocatorError> {
110        let (x, y) = if self.force {
111            let info = self.locator.query_element_info().await?;
112            if !info.found {
113                return Err(LocatorError::NotFound(format!("{:?}", self.locator.selector)));
114            }
115            
116            if let Some((offset_x, offset_y)) = self.position {
117                (info.x.unwrap_or(0.0) + offset_x, info.y.unwrap_or(0.0) + offset_y)
118            } else {
119                (
120                    info.x.unwrap_or(0.0) + info.width.unwrap_or(0.0) / 2.0,
121                    info.y.unwrap_or(0.0) + info.height.unwrap_or(0.0) / 2.0,
122                )
123            }
124        } else {
125            let info = self.locator.wait_for_actionable().await?;
126            
127            if let Some((offset_x, offset_y)) = self.position {
128                (
129                    info.x.expect("visible element has x") + offset_x,
130                    info.y.expect("visible element has y") + offset_y,
131                )
132            } else {
133                (
134                    info.x.expect("visible element has x") + info.width.expect("visible element has width") / 2.0,
135                    info.y.expect("visible element has y") + info.height.expect("visible element has height") / 2.0,
136                )
137            }
138        };
139
140        debug!(x, y, button = ?self.button, modifiers = self.modifiers, click_count = self.click_count, "Clicking element");
141
142        // Move to element
143        let mut move_event = DispatchMouseEventParams::mouse_move(x, y);
144        if self.modifiers != 0 {
145            move_event.modifiers = Some(self.modifiers);
146        }
147        self.locator.dispatch_mouse_event(move_event).await?;
148
149        // Mouse down
150        let mut down_event = DispatchMouseEventParams::mouse_down(x, y, self.button);
151        if self.modifiers != 0 {
152            down_event.modifiers = Some(self.modifiers);
153        }
154        down_event.click_count = Some(self.click_count);
155        self.locator.dispatch_mouse_event(down_event).await?;
156
157        // Mouse up
158        let mut up_event = DispatchMouseEventParams::mouse_up(x, y, self.button);
159        if self.modifiers != 0 {
160            up_event.modifiers = Some(self.modifiers);
161        }
162        up_event.click_count = Some(self.click_count);
163        self.locator.dispatch_mouse_event(up_event).await?;
164
165        Ok(())
166    }
167}
168
169impl<'l> std::future::IntoFuture for ClickBuilder<'l, '_> {
170    type Output = Result<(), LocatorError>;
171    type IntoFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'l>>;
172
173    fn into_future(self) -> Self::IntoFuture {
174        Box::pin(self.send())
175    }
176}
177
178// =============================================================================
179// TypeBuilder
180// =============================================================================
181
182/// Builder for type operations with configurable options.
183///
184/// Created via [`Locator::type_text`].
185#[derive(Debug)]
186pub struct TypeBuilder<'l, 'a> {
187    locator: &'l Locator<'a>,
188    text: String,
189    delay: Option<Duration>,
190}
191
192impl<'l, 'a> TypeBuilder<'l, 'a> {
193    pub(crate) fn new(locator: &'l Locator<'a>, text: &str) -> Self {
194        Self {
195            locator,
196            text: text.to_string(),
197            delay: None,
198        }
199    }
200
201    /// Set the delay between characters.
202    #[must_use]
203    pub fn delay(mut self, delay: Duration) -> Self {
204        self.delay = Some(delay);
205        self
206    }
207
208    /// Execute the type operation.
209    #[instrument(level = "debug", skip(self), fields(selector = ?self.locator.selector))]
210    pub async fn send(self) -> Result<(), LocatorError> {
211        self.locator.wait_for_actionable().await?;
212
213        debug!(text = %self.text, delay = ?self.delay, "Typing text");
214
215        self.locator.focus_element().await?;
216
217        for ch in self.text.chars() {
218            let char_str = ch.to_string();
219            self.locator.dispatch_key_event(DispatchKeyEventParams::char(&char_str)).await?;
220            
221            if let Some(delay) = self.delay {
222                tokio::time::sleep(delay).await;
223            }
224        }
225
226        Ok(())
227    }
228}
229
230impl<'l> std::future::IntoFuture for TypeBuilder<'l, '_> {
231    type Output = Result<(), LocatorError>;
232    type IntoFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'l>>;
233
234    fn into_future(self) -> Self::IntoFuture {
235        Box::pin(self.send())
236    }
237}
238
239// =============================================================================
240// HoverBuilder
241// =============================================================================
242
243/// Builder for hover operations with configurable options.
244///
245/// Created via [`Locator::hover`].
246#[derive(Debug)]
247pub struct HoverBuilder<'l, 'a> {
248    locator: &'l Locator<'a>,
249    position: Option<(f64, f64)>,
250    modifiers: i32,
251    force: bool,
252}
253
254impl<'l, 'a> HoverBuilder<'l, 'a> {
255    pub(crate) fn new(locator: &'l Locator<'a>) -> Self {
256        Self {
257            locator,
258            position: None,
259            modifiers: 0,
260            force: false,
261        }
262    }
263
264    /// Set the position offset from the element's top-left corner.
265    #[must_use]
266    pub fn position(mut self, x: f64, y: f64) -> Self {
267        self.position = Some((x, y));
268        self
269    }
270
271    /// Set modifier keys to hold during hover.
272    #[must_use]
273    pub fn modifiers(mut self, modifiers: i32) -> Self {
274        self.modifiers = modifiers;
275        self
276    }
277
278    /// Whether to bypass actionability checks.
279    #[must_use]
280    pub fn force(mut self, force: bool) -> Self {
281        self.force = force;
282        self
283    }
284
285    /// Execute the hover operation.
286    #[instrument(level = "debug", skip(self), fields(selector = ?self.locator.selector))]
287    pub async fn send(self) -> Result<(), LocatorError> {
288        let (x, y) = if self.force {
289            let info = self.locator.query_element_info().await?;
290            if !info.found {
291                return Err(LocatorError::NotFound(format!("{:?}", self.locator.selector)));
292            }
293            
294            if let Some((offset_x, offset_y)) = self.position {
295                (info.x.unwrap_or(0.0) + offset_x, info.y.unwrap_or(0.0) + offset_y)
296            } else {
297                (
298                    info.x.unwrap_or(0.0) + info.width.unwrap_or(0.0) / 2.0,
299                    info.y.unwrap_or(0.0) + info.height.unwrap_or(0.0) / 2.0,
300                )
301            }
302        } else {
303            let info = self.locator.wait_for_actionable().await?;
304            
305            if let Some((offset_x, offset_y)) = self.position {
306                (
307                    info.x.expect("visible element has x") + offset_x,
308                    info.y.expect("visible element has y") + offset_y,
309                )
310            } else {
311                (
312                    info.x.expect("visible element has x") + info.width.expect("visible element has width") / 2.0,
313                    info.y.expect("visible element has y") + info.height.expect("visible element has height") / 2.0,
314                )
315            }
316        };
317
318        debug!(x, y, modifiers = self.modifiers, "Hovering over element");
319
320        let mut move_event = DispatchMouseEventParams::mouse_move(x, y);
321        if self.modifiers != 0 {
322            move_event.modifiers = Some(self.modifiers);
323        }
324        self.locator.dispatch_mouse_event(move_event).await?;
325
326        Ok(())
327    }
328}
329
330impl<'l> std::future::IntoFuture for HoverBuilder<'l, '_> {
331    type Output = Result<(), LocatorError>;
332    type IntoFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'l>>;
333
334    fn into_future(self) -> Self::IntoFuture {
335        Box::pin(self.send())
336    }
337}
338
339// =============================================================================
340// TapBuilder
341// =============================================================================
342
343/// Builder for tap operations with configurable options.
344///
345/// Created via [`Locator::tap`].
346#[derive(Debug)]
347pub struct TapBuilder<'l, 'a> {
348    locator: &'l Locator<'a>,
349    position: Option<(f64, f64)>,
350    force: bool,
351    modifiers: i32,
352}
353
354impl<'l, 'a> TapBuilder<'l, 'a> {
355    pub(crate) fn new(locator: &'l Locator<'a>) -> Self {
356        Self {
357            locator,
358            position: None,
359            force: false,
360            modifiers: 0,
361        }
362    }
363
364    /// Set the position offset from the element's top-left corner.
365    #[must_use]
366    pub fn position(mut self, x: f64, y: f64) -> Self {
367        self.position = Some((x, y));
368        self
369    }
370
371    /// Whether to bypass actionability checks.
372    #[must_use]
373    pub fn force(mut self, force: bool) -> Self {
374        self.force = force;
375        self
376    }
377
378    /// Set modifier keys to hold during the tap.
379    #[must_use]
380    pub fn modifiers(mut self, modifiers: i32) -> Self {
381        self.modifiers = modifiers;
382        self
383    }
384
385    /// Execute the tap operation.
386    #[instrument(level = "debug", skip(self), fields(selector = ?self.locator.selector))]
387    pub async fn send(self) -> Result<(), LocatorError> {
388        let (x, y) = if self.force {
389            let info = self.locator.query_element_info().await?;
390            if !info.found {
391                return Err(LocatorError::NotFound(format!("{:?}", self.locator.selector)));
392            }
393            
394            if let Some((offset_x, offset_y)) = self.position {
395                (info.x.unwrap_or(0.0) + offset_x, info.y.unwrap_or(0.0) + offset_y)
396            } else {
397                (
398                    info.x.unwrap_or(0.0) + info.width.unwrap_or(0.0) / 2.0,
399                    info.y.unwrap_or(0.0) + info.height.unwrap_or(0.0) / 2.0,
400                )
401            }
402        } else {
403            let info = self.locator.wait_for_actionable().await?;
404            
405            if let Some((offset_x, offset_y)) = self.position {
406                (
407                    info.x.expect("visible element has x") + offset_x,
408                    info.y.expect("visible element has y") + offset_y,
409                )
410            } else {
411                (
412                    info.x.expect("visible element has x") + info.width.expect("visible element has width") / 2.0,
413                    info.y.expect("visible element has y") + info.height.expect("visible element has height") / 2.0,
414                )
415            }
416        };
417
418        debug!(x, y, modifiers = self.modifiers, "Tapping element");
419
420        if self.modifiers != 0 {
421            self.locator.page.touchscreen().tap_with_modifiers(x, y, self.modifiers).await
422        } else {
423            self.locator.page.touchscreen().tap(x, y).await
424        }
425    }
426}