viewpoint_core/page/locator/debug/
mod.rs

1//! Debug and visualization methods for locators.
2//!
3//! Methods for highlighting and debugging element selections.
4
5use std::time::Duration;
6
7use tracing::{debug, instrument};
8use viewpoint_js::js;
9
10use super::Locator;
11use crate::error::LocatorError;
12
13impl Locator<'_> {
14    /// Highlight the element for debugging purposes.
15    ///
16    /// This visually highlights the element with a magenta outline for 2 seconds,
17    /// making it easy to verify which element is being targeted.
18    ///
19    /// # Example
20    ///
21    /// ```no_run
22    /// use std::time::Duration;
23    /// use viewpoint_core::Page;
24    ///
25    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
26    /// // Highlight for default duration (2 seconds)
27    /// page.locator("button").highlight().await?;
28    ///
29    /// // Highlight for custom duration
30    /// page.locator("button").highlight_for(Duration::from_secs(5)).await?;
31    /// # Ok(())
32    /// # }
33    /// ```
34    ///
35    /// # Errors
36    ///
37    /// Returns an error if the element cannot be found or highlighted.
38    #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
39    pub async fn highlight(&self) -> Result<(), LocatorError> {
40        self.highlight_for(Duration::from_secs(2)).await
41    }
42
43    /// Highlight the element for a specific duration.
44    ///
45    /// # Arguments
46    ///
47    /// * `duration` - How long to show the highlight.
48    ///
49    /// # Errors
50    ///
51    /// Returns an error if the element cannot be found or highlighted.
52    #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
53    pub async fn highlight_for(&self, duration: Duration) -> Result<(), LocatorError> {
54        self.wait_for_actionable().await?;
55
56        debug!(?duration, "Highlighting element");
57
58        // Add highlight style
59        let selector_expr = self.selector.to_js_expression();
60        let highlight_js = js! {
61            (function() {
62                const elements = @{selector_expr};
63                if (elements.length === 0) return { found: false };
64                
65                const el = elements[0];
66                const originalOutline = el.style.outline;
67                const originalOutlineOffset = el.style.outlineOffset;
68                const originalTransition = el.style.transition;
69                
70                // Apply highlight with animation
71                el.style.transition = "outline 0.2s ease-in-out";
72                el.style.outline = "3px solid #ff00ff";
73                el.style.outlineOffset = "2px";
74                
75                // Store original styles for restoration
76                el.__viewpoint_original_outline = originalOutline;
77                el.__viewpoint_original_outline_offset = originalOutlineOffset;
78                el.__viewpoint_original_transition = originalTransition;
79                
80                return { found: true };
81            })()
82        };
83
84        let result = self.evaluate_js(&highlight_js).await?;
85        let found = result
86            .get("found")
87            .and_then(serde_json::Value::as_bool)
88            .unwrap_or(false);
89        if !found {
90            return Err(LocatorError::NotFound(format!("{:?}", self.selector)));
91        }
92
93        // Wait for the duration
94        tokio::time::sleep(duration).await;
95
96        // Remove highlight
97        let cleanup_js = js! {
98            (function() {
99                const elements = @{selector_expr};
100                if (elements.length === 0) return;
101                
102                const el = elements[0];
103                el.style.outline = el.__viewpoint_original_outline || "";
104                el.style.outlineOffset = el.__viewpoint_original_outline_offset || "";
105                el.style.transition = el.__viewpoint_original_transition || "";
106                
107                delete el.__viewpoint_original_outline;
108                delete el.__viewpoint_original_outline_offset;
109                delete el.__viewpoint_original_transition;
110            })()
111        };
112
113        // Ignore cleanup errors - element may have been removed
114        let _ = self.evaluate_js(&cleanup_js).await;
115
116        Ok(())
117    }
118}