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