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}