Skip to main content

browser_commander/elements/
visibility.rs

1//! Element visibility utilities.
2//!
3//! This module provides utilities for checking element visibility
4//! and viewport positioning.
5
6use crate::core::engine::{EngineAdapter, EngineError};
7
8/// Options for checking element visibility.
9#[derive(Debug, Clone)]
10pub struct VisibilityOptions {
11    /// The selector for the element to check.
12    pub selector: String,
13    /// Timeout for visibility check in milliseconds.
14    pub timeout_ms: Option<u64>,
15}
16
17/// Result of a visibility check.
18#[derive(Debug, Clone)]
19pub struct VisibilityResult {
20    /// Whether the element is visible.
21    pub visible: bool,
22    /// Whether the element exists in the DOM.
23    pub exists: bool,
24    /// Whether the element is in the viewport.
25    pub in_viewport: bool,
26}
27
28/// Check if an element is visible using the provided engine adapter.
29///
30/// # Arguments
31///
32/// * `adapter` - The engine adapter to use
33/// * `selector` - The CSS selector for the element
34///
35/// # Returns
36///
37/// `true` if the element is visible, `false` otherwise
38pub async fn is_visible(adapter: &dyn EngineAdapter, selector: &str) -> Result<bool, EngineError> {
39    adapter.is_visible(selector).await
40}
41
42/// Check if an element is enabled (for form elements).
43///
44/// # Arguments
45///
46/// * `adapter` - The engine adapter to use
47/// * `selector` - The CSS selector for the element
48///
49/// # Returns
50///
51/// `true` if the element is enabled, `false` otherwise
52pub async fn is_enabled(adapter: &dyn EngineAdapter, selector: &str) -> Result<bool, EngineError> {
53    adapter.is_enabled(selector).await
54}
55
56/// Count the number of elements matching a selector.
57///
58/// # Arguments
59///
60/// * `adapter` - The engine adapter to use
61/// * `selector` - The CSS selector to count
62///
63/// # Returns
64///
65/// The number of matching elements
66pub async fn count(adapter: &dyn EngineAdapter, selector: &str) -> Result<usize, EngineError> {
67    adapter.count(selector).await
68}
69
70/// Calculate if an element is within the viewport.
71///
72/// # Arguments
73///
74/// * `bounding_box` - The element's bounding box (x, y, width, height)
75/// * `viewport_width` - The viewport width
76/// * `viewport_height` - The viewport height
77/// * `margin` - Additional margin to consider element visible
78///
79/// # Returns
80///
81/// `true` if the element is in the viewport
82pub fn is_in_viewport(
83    bounding_box: (f64, f64, f64, f64),
84    viewport_width: f64,
85    viewport_height: f64,
86    margin: f64,
87) -> bool {
88    let (x, y, width, height) = bounding_box;
89
90    // Check if element is at least partially visible with margin
91    let in_vertical = y < viewport_height - margin && (y + height) > margin;
92    let in_horizontal = x < viewport_width - margin && (x + width) > margin;
93
94    in_vertical && in_horizontal
95}
96
97/// Calculate if scrolling is needed to center an element.
98///
99/// # Arguments
100///
101/// * `bounding_box` - The element's bounding box (x, y, width, height)
102/// * `viewport_height` - The viewport height
103/// * `threshold_percent` - Percentage of viewport height to consider "significant"
104///
105/// # Returns
106///
107/// `true` if scrolling is needed
108pub fn needs_scrolling(
109    bounding_box: (f64, f64, f64, f64),
110    viewport_height: f64,
111    threshold_percent: f64,
112) -> bool {
113    let (_, y, _, height) = bounding_box;
114
115    let element_center = y + height / 2.0;
116    let viewport_center = viewport_height / 2.0;
117    let distance_from_center = (element_center - viewport_center).abs();
118    let threshold_pixels = (viewport_height * threshold_percent) / 100.0;
119
120    // Check if element is visible and within threshold
121    let is_visible = y >= 0.0 && (y + height) <= viewport_height;
122    let is_within_threshold = distance_from_center <= threshold_pixels;
123
124    !is_visible || !is_within_threshold
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn is_in_viewport_fully_visible() {
133        // Element in the center of viewport
134        assert!(is_in_viewport(
135            (100.0, 100.0, 50.0, 50.0),
136            800.0,
137            600.0,
138            0.0
139        ));
140    }
141
142    #[test]
143    fn is_in_viewport_partially_visible() {
144        // Element partially visible at the edge (right side visible)
145        // x=60, so right edge is at 110, which is > margin of 50
146        assert!(is_in_viewport(
147            (60.0, 100.0, 50.0, 50.0),
148            800.0,
149            600.0,
150            50.0
151        ));
152        // Element partially visible at left edge (without margin requirement)
153        assert!(is_in_viewport(
154            (-10.0, 100.0, 50.0, 50.0),
155            800.0,
156            600.0,
157            0.0
158        ));
159    }
160
161    #[test]
162    fn is_in_viewport_not_visible() {
163        // Element completely above viewport
164        assert!(!is_in_viewport(
165            (100.0, -200.0, 50.0, 50.0),
166            800.0,
167            600.0,
168            50.0
169        ));
170        // Element completely below viewport
171        assert!(!is_in_viewport(
172            (100.0, 700.0, 50.0, 50.0),
173            800.0,
174            600.0,
175            50.0
176        ));
177    }
178
179    #[test]
180    fn needs_scrolling_element_centered() {
181        // Element near the center of viewport - no scroll needed
182        let viewport_height = 600.0;
183        let element_y = 270.0; // Near center (300 - 30)
184        let element_height = 60.0;
185
186        assert!(!needs_scrolling(
187            (0.0, element_y, 100.0, element_height),
188            viewport_height,
189            10.0
190        ));
191    }
192
193    #[test]
194    fn needs_scrolling_element_at_top() {
195        // Element at the top of viewport - scroll needed
196        let viewport_height = 600.0;
197
198        assert!(needs_scrolling(
199            (0.0, 10.0, 100.0, 50.0),
200            viewport_height,
201            10.0
202        ));
203    }
204
205    #[test]
206    fn needs_scrolling_element_at_bottom() {
207        // Element at the bottom of viewport - scroll needed
208        let viewport_height = 600.0;
209
210        assert!(needs_scrolling(
211            (0.0, 540.0, 100.0, 50.0),
212            viewport_height,
213            10.0
214        ));
215    }
216
217    #[test]
218    fn needs_scrolling_element_outside_viewport() {
219        // Element completely below viewport - scroll needed
220        let viewport_height = 600.0;
221
222        assert!(needs_scrolling(
223            (0.0, 700.0, 100.0, 50.0),
224            viewport_height,
225            10.0
226        ));
227    }
228}