viewpoint_core/page/locator/builders/
click.rs

1//! Click builder for locator actions.
2
3use tracing::{debug, instrument, trace};
4use viewpoint_cdp::protocol::input::{DispatchMouseEventParams, MouseButton};
5
6use super::super::Locator;
7use crate::error::LocatorError;
8use crate::wait::NavigationWaiter;
9
10/// Builder for click operations with configurable options.
11///
12/// Created via [`Locator::click`].
13///
14/// # Example
15///
16/// ```
17/// # #[cfg(feature = "integration")]
18/// # tokio_test::block_on(async {
19/// # use viewpoint_core::Browser;
20/// # let browser = Browser::launch().headless(true).launch().await.unwrap();
21/// # let context = browser.new_context().await.unwrap();
22/// # let page = context.new_page().await.unwrap();
23/// # page.goto("about:blank").goto().await.unwrap();
24///
25/// // Simple click - await directly
26/// page.locator("button").click().await.ok();
27///
28/// // Force click without waiting for actionability
29/// page.locator("button").click().force(true).await.ok();
30///
31/// // Click without waiting for navigation
32/// page.locator("a").click().no_wait_after(true).await.ok();
33/// # });
34/// ```
35#[derive(Debug)]
36pub struct ClickBuilder<'l, 'a> {
37    locator: &'l Locator<'a>,
38    /// Position offset from element's top-left corner.
39    position: Option<(f64, f64)>,
40    /// Mouse button to use.
41    button: MouseButton,
42    /// Modifier keys to hold during the click.
43    modifiers: i32,
44    /// Whether to bypass actionability checks.
45    force: bool,
46    /// Click count (1 for single click, 2 for double click).
47    click_count: i32,
48    /// Whether to skip waiting for navigation after the action.
49    no_wait_after: bool,
50}
51
52impl<'l, 'a> ClickBuilder<'l, 'a> {
53    pub(crate) fn new(locator: &'l Locator<'a>) -> Self {
54        Self {
55            locator,
56            position: None,
57            button: MouseButton::Left,
58            modifiers: 0,
59            force: false,
60            click_count: 1,
61            no_wait_after: false,
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    /// Whether to skip waiting for navigation after the click.
108    ///
109    /// By default, the click will wait for any triggered navigation to complete.
110    /// Set to `true` to return immediately after the click is performed.
111    ///
112    /// # Example
113    ///
114    /// ```no_run
115    /// use viewpoint_core::Page;
116    ///
117    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
118    /// // Click a link but don't wait for navigation
119    /// page.locator("a").click().no_wait_after(true).await?;
120    /// # Ok(())
121    /// # }
122    /// ```
123    #[must_use]
124    pub fn no_wait_after(mut self, no_wait_after: bool) -> Self {
125        self.no_wait_after = no_wait_after;
126        self
127    }
128
129    /// Execute the click operation.
130    #[instrument(level = "debug", skip(self), fields(selector = ?self.locator.selector))]
131    pub async fn send(self) -> Result<(), LocatorError> {
132        // Set up navigation waiter before the action if needed
133        let navigation_waiter = if self.no_wait_after {
134            None
135        } else {
136            Some(NavigationWaiter::new(
137                self.locator.page.connection().subscribe_events(),
138                self.locator.page.session_id().to_string(),
139                self.locator.page.frame_id().to_string(),
140            ))
141        };
142
143        // Perform the click action
144        self.perform_click().await?;
145
146        // Wait for navigation if triggered
147        if let Some(waiter) = navigation_waiter {
148            match waiter.wait_for_navigation_if_triggered().await {
149                Ok(navigated) => {
150                    if navigated {
151                        trace!("Navigation completed after click");
152                    }
153                }
154                Err(e) => {
155                    debug!(error = ?e, "Navigation wait failed after click");
156                    return Err(LocatorError::WaitError(e));
157                }
158            }
159        }
160
161        Ok(())
162    }
163
164    /// Perform the actual click without navigation waiting.
165    async fn perform_click(&self) -> Result<(), LocatorError> {
166        let (x, y) = if self.force {
167            let info = self.locator.query_element_info().await?;
168            if !info.found {
169                return Err(LocatorError::NotFound(format!(
170                    "{:?}",
171                    self.locator.selector
172                )));
173            }
174
175            if let Some((offset_x, offset_y)) = self.position {
176                (
177                    info.x.unwrap_or(0.0) + offset_x,
178                    info.y.unwrap_or(0.0) + offset_y,
179                )
180            } else {
181                (
182                    info.x.unwrap_or(0.0) + info.width.unwrap_or(0.0) / 2.0,
183                    info.y.unwrap_or(0.0) + info.height.unwrap_or(0.0) / 2.0,
184                )
185            }
186        } else {
187            let info = self.locator.wait_for_actionable().await?;
188
189            if let Some((offset_x, offset_y)) = self.position {
190                (
191                    info.x.expect("visible element has x") + offset_x,
192                    info.y.expect("visible element has y") + offset_y,
193                )
194            } else {
195                (
196                    info.x.expect("visible element has x")
197                        + info.width.expect("visible element has width") / 2.0,
198                    info.y.expect("visible element has y")
199                        + info.height.expect("visible element has height") / 2.0,
200                )
201            }
202        };
203
204        debug!(x, y, button = ?self.button, modifiers = self.modifiers, click_count = self.click_count, "Clicking element");
205
206        // Move to element
207        let mut move_event = DispatchMouseEventParams::mouse_move(x, y);
208        if self.modifiers != 0 {
209            move_event.modifiers = Some(self.modifiers);
210        }
211        self.locator.dispatch_mouse_event(move_event).await?;
212
213        // Mouse down
214        let mut down_event = DispatchMouseEventParams::mouse_down(x, y, self.button);
215        if self.modifiers != 0 {
216            down_event.modifiers = Some(self.modifiers);
217        }
218        down_event.click_count = Some(self.click_count);
219        self.locator.dispatch_mouse_event(down_event).await?;
220
221        // Mouse up
222        let mut up_event = DispatchMouseEventParams::mouse_up(x, y, self.button);
223        if self.modifiers != 0 {
224            up_event.modifiers = Some(self.modifiers);
225        }
226        up_event.click_count = Some(self.click_count);
227        self.locator.dispatch_mouse_event(up_event).await?;
228
229        Ok(())
230    }
231}
232
233impl<'l> std::future::IntoFuture for ClickBuilder<'l, '_> {
234    type Output = Result<(), LocatorError>;
235    type IntoFuture =
236        std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'l>>;
237
238    fn into_future(self) -> Self::IntoFuture {
239        Box::pin(self.send())
240    }
241}