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