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