viewpoint_core/page/locator_handler/
mod.rs

1//! Locator handlers for handling overlay elements that block actions.
2//!
3//! This module provides functionality for registering handlers that automatically
4//! dismiss overlay elements (like cookie banners, notifications, etc.) that may
5//! interfere with page interactions.
6
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::Arc;
10
11use tokio::sync::RwLock;
12use tracing::{debug, instrument, warn};
13
14use super::Page;
15use super::locator::{Locator, Selector};
16use crate::error::LocatorError;
17
18/// Type alias for locator handler function.
19pub type LocatorHandlerFn =
20    Arc<dyn Fn() -> Pin<Box<dyn Future<Output = Result<(), LocatorError>> + Send>> + Send + Sync>;
21
22/// Options for locator handlers.
23#[derive(Debug, Clone, Default)]
24pub struct LocatorHandlerOptions {
25    /// Whether to skip waiting after the handler runs.
26    pub no_wait_after: bool,
27    /// Maximum number of times the handler can run. None means unlimited.
28    pub times: Option<u32>,
29}
30
31/// Internal representation of a registered handler.
32#[derive(Clone)]
33struct RegisteredHandler {
34    /// A unique ID for this handler.
35    id: u64,
36    /// The selector to match.
37    selector: Selector,
38    /// The handler function.
39    handler: LocatorHandlerFn,
40    /// Options for the handler.
41    options: LocatorHandlerOptions,
42    /// Number of times the handler has run.
43    run_count: u32,
44}
45
46impl std::fmt::Debug for RegisteredHandler {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.debug_struct("RegisteredHandler")
49            .field("id", &self.id)
50            .field("selector", &self.selector)
51            .field("options", &self.options)
52            .field("run_count", &self.run_count)
53            .finish()
54    }
55}
56
57/// Manager for locator handlers.
58#[derive(Debug)]
59pub struct LocatorHandlerManager {
60    /// Registered handlers.
61    handlers: RwLock<Vec<RegisteredHandler>>,
62    /// Counter for generating unique handler IDs.
63    next_id: std::sync::atomic::AtomicU64,
64}
65
66impl LocatorHandlerManager {
67    /// Create a new locator handler manager.
68    pub fn new() -> Self {
69        Self {
70            handlers: RwLock::new(Vec::new()),
71            next_id: std::sync::atomic::AtomicU64::new(1),
72        }
73    }
74
75    /// Add a locator handler.
76    ///
77    /// The handler will be called when the specified locator matches an element
78    /// that is blocking an action. Returns the handler ID.
79    #[instrument(level = "debug", skip(self, handler), fields(selector = ?selector))]
80    pub async fn add_handler<F, Fut>(
81        &self,
82        selector: Selector,
83        handler: F,
84        options: LocatorHandlerOptions,
85    ) -> u64
86    where
87        F: Fn() -> Fut + Send + Sync + 'static,
88        Fut: Future<Output = Result<(), LocatorError>> + Send + 'static,
89    {
90        let id = self
91            .next_id
92            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
93        let handler_fn: LocatorHandlerFn = Arc::new(move || Box::pin(handler()));
94
95        let registered = RegisteredHandler {
96            id,
97            selector,
98            handler: handler_fn,
99            options,
100            run_count: 0,
101        };
102
103        let mut handlers = self.handlers.write().await;
104        handlers.push(registered);
105        debug!(handler_id = id, "Locator handler registered");
106        id
107    }
108
109    /// Remove a locator handler by ID.
110    #[instrument(level = "debug", skip(self))]
111    pub async fn remove_handler_by_id(&self, id: u64) {
112        let mut handlers = self.handlers.write().await;
113        let initial_len = handlers.len();
114        handlers.retain(|h| h.id != id);
115
116        if handlers.len() < initial_len {
117            debug!(handler_id = id, "Locator handler removed");
118        } else {
119            debug!(handler_id = id, "No matching locator handler found");
120        }
121    }
122
123    /// Check if any handler matches a blocking element and run it.
124    ///
125    /// Returns true if a handler was run.
126    #[instrument(level = "debug", skip(self, page))]
127    pub async fn try_handle_blocking(&self, page: &Page) -> bool {
128        let handlers = self.handlers.read().await;
129
130        for handler in handlers.iter() {
131            // Check if the selector matches a visible element
132            let locator = Locator::new(page, handler.selector.clone());
133
134            if let Ok(is_visible) = locator.is_visible().await {
135                if is_visible {
136                    let handler_id = handler.id;
137                    debug!(
138                        handler_id = handler_id,
139                        "Handler selector matched, running handler"
140                    );
141                    let handler_fn = handler.handler.clone();
142                    drop(handlers); // Release read lock before running handler
143
144                    if let Err(e) = handler_fn().await {
145                        warn!(handler_id = handler_id, "Locator handler failed: {}", e);
146                    } else {
147                        // Increment run count and check if we should remove
148                        let mut handlers = self.handlers.write().await;
149                        if let Some(handler) = handlers.iter_mut().find(|h| h.id == handler_id) {
150                            handler.run_count += 1;
151
152                            if let Some(times) = handler.options.times {
153                                if handler.run_count >= times {
154                                    debug!(
155                                        handler_id = handler_id,
156                                        "Handler reached times limit, removing"
157                                    );
158                                    handlers.retain(|h| h.id != handler_id);
159                                }
160                            }
161                        }
162
163                        return true;
164                    }
165
166                    return false;
167                }
168            }
169        }
170
171        false
172    }
173}
174
175impl Default for LocatorHandlerManager {
176    fn default() -> Self {
177        Self::new()
178    }
179}
180
181/// A handle to a registered locator handler.
182///
183/// Use this to remove the handler later.
184#[derive(Debug, Clone, Copy)]
185pub struct LocatorHandlerHandle {
186    id: u64,
187}
188
189impl LocatorHandlerHandle {
190    /// Create a new handle from an ID.
191    pub(crate) fn new(id: u64) -> Self {
192        Self { id }
193    }
194
195    /// Get the handler ID.
196    pub fn id(&self) -> u64 {
197        self.id
198    }
199}
200
201// Page impl for locator handler methods
202impl super::Page {
203    /// Add a handler for overlay elements that may block actions.
204    ///
205    /// This is useful for automatically dismissing elements like cookie banners,
206    /// notification popups, or other overlays that appear during tests.
207    ///
208    /// # Example
209    ///
210    /// ```no_run
211    /// use viewpoint_core::{Page, AriaRole};
212    /// use std::sync::Arc;
213    ///
214    /// # async fn example(page: Arc<Page>) -> Result<(), viewpoint_core::CoreError> {
215    /// // Dismiss cookie banner when it appears
216    /// let page_clone = page.clone();
217    /// let handle = page.add_locator_handler(
218    ///     page.get_by_role(AriaRole::Button).with_name("Accept cookies"),
219    ///     move || {
220    ///         let page = page_clone.clone();
221    ///         async move { page.locator(".cookie-banner .accept").click().await }
222    ///     }
223    /// ).await;
224    ///
225    /// // Later, remove the handler
226    /// page.remove_locator_handler(handle).await;
227    /// # Ok(())
228    /// # }
229    /// ```
230    pub async fn add_locator_handler<F, Fut>(
231        &self,
232        locator: impl Into<super::Locator<'_>>,
233        handler: F,
234    ) -> LocatorHandlerHandle
235    where
236        F: Fn() -> Fut + Send + Sync + 'static,
237        Fut: std::future::Future<Output = Result<(), crate::error::LocatorError>> + Send + 'static,
238    {
239        let loc = locator.into();
240        let id = self
241            .locator_handler_manager
242            .add_handler(
243                loc.selector().clone(),
244                handler,
245                LocatorHandlerOptions::default(),
246            )
247            .await;
248        LocatorHandlerHandle::new(id)
249    }
250
251    /// Add a locator handler with options.
252    ///
253    /// # Example
254    ///
255    /// ```no_run
256    /// use viewpoint_core::{Page, LocatorHandlerOptions};
257    /// use std::sync::Arc;
258    ///
259    /// # async fn example(page: Arc<Page>) -> Result<(), viewpoint_core::CoreError> {
260    /// // Handler that only runs once
261    /// let page_clone = page.clone();
262    /// page.add_locator_handler_with_options(
263    ///     page.locator(".popup"),
264    ///     move || {
265    ///         let page = page_clone.clone();
266    ///         async move { page.locator(".popup .close").click().await }
267    ///     },
268    ///     LocatorHandlerOptions { times: Some(1), ..Default::default() }
269    /// ).await;
270    /// # Ok(())
271    /// # }
272    /// ```
273    pub async fn add_locator_handler_with_options<F, Fut>(
274        &self,
275        locator: impl Into<super::Locator<'_>>,
276        handler: F,
277        options: LocatorHandlerOptions,
278    ) -> LocatorHandlerHandle
279    where
280        F: Fn() -> Fut + Send + Sync + 'static,
281        Fut: std::future::Future<Output = Result<(), crate::error::LocatorError>> + Send + 'static,
282    {
283        let loc = locator.into();
284        let id = self
285            .locator_handler_manager
286            .add_handler(loc.selector().clone(), handler, options)
287            .await;
288        LocatorHandlerHandle::new(id)
289    }
290
291    /// Remove a locator handler.
292    pub async fn remove_locator_handler(&self, handle: LocatorHandlerHandle) {
293        self.locator_handler_manager
294            .remove_handler_by_id(handle.id())
295            .await;
296    }
297
298    /// Get the locator handler manager.
299    pub(crate) fn locator_handler_manager(&self) -> &LocatorHandlerManager {
300        &self.locator_handler_manager
301    }
302}