Skip to main content

browser_commander/interactions/
scroll.rs

1//! Scroll operations for browser automation.
2//!
3//! This module provides utilities for scrolling elements into view
4//! with verification support.
5
6use crate::core::constants::TIMING;
7use crate::core::engine::{EngineAdapter, EngineError, ScrollVerificationResult};
8use std::time::{Duration, Instant};
9
10/// Scroll behavior options.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum ScrollBehavior {
13    /// Smooth animated scrolling.
14    #[default]
15    Smooth,
16    /// Instant scrolling (no animation).
17    Instant,
18}
19
20impl std::fmt::Display for ScrollBehavior {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        match self {
23            ScrollBehavior::Smooth => write!(f, "smooth"),
24            ScrollBehavior::Instant => write!(f, "instant"),
25        }
26    }
27}
28
29/// Options for scroll operations.
30#[derive(Debug, Clone)]
31pub struct ScrollOptions {
32    /// The scroll behavior (smooth or instant).
33    pub behavior: ScrollBehavior,
34    /// Whether to verify the scroll operation.
35    pub verify: bool,
36    /// Timeout for verification.
37    pub verification_timeout: Duration,
38    /// Interval between verification retries.
39    pub verification_retry_interval: Duration,
40    /// Threshold percentage for determining if scroll is needed.
41    pub threshold_percent: f64,
42    /// Wait time after scroll for animation to complete.
43    pub wait_after_scroll: Duration,
44}
45
46impl Default for ScrollOptions {
47    fn default() -> Self {
48        Self {
49            behavior: ScrollBehavior::Smooth,
50            verify: true,
51            verification_timeout: TIMING.verification_timeout,
52            verification_retry_interval: TIMING.verification_retry_interval,
53            threshold_percent: 10.0,
54            wait_after_scroll: TIMING.scroll_animation_wait,
55        }
56    }
57}
58
59/// Result of a scroll operation.
60#[derive(Debug, Clone)]
61pub struct ScrollResult {
62    /// Whether scroll was performed.
63    pub scrolled: bool,
64    /// Whether the scroll was verified as successful.
65    pub verified: bool,
66    /// Whether the scroll was skipped because element was already in view.
67    pub skipped: bool,
68}
69
70impl ScrollResult {
71    /// Create a result indicating scroll was skipped.
72    pub fn skipped() -> Self {
73        Self {
74            scrolled: false,
75            verified: true,
76            skipped: true,
77        }
78    }
79
80    /// Create a result indicating scroll was performed.
81    pub fn performed(verified: bool) -> Self {
82        Self {
83            scrolled: true,
84            verified,
85            skipped: false,
86        }
87    }
88
89    /// Create a result indicating scroll failed (e.g., navigation occurred).
90    pub fn failed() -> Self {
91        Self {
92            scrolled: false,
93            verified: false,
94            skipped: false,
95        }
96    }
97}
98
99/// Scroll an element into view.
100///
101/// # Arguments
102///
103/// * `adapter` - The engine adapter to use
104/// * `selector` - The CSS selector for the element
105/// * `options` - Scroll options
106///
107/// # Returns
108///
109/// The result of the scroll operation
110pub async fn scroll_into_view(
111    adapter: &dyn EngineAdapter,
112    selector: &str,
113    options: &ScrollOptions,
114) -> Result<ScrollResult, EngineError> {
115    adapter.scroll_into_view(selector).await?;
116
117    if options.verify {
118        let verification = verify_scroll(adapter, selector, options).await?;
119        Ok(ScrollResult::performed(verification.verified))
120    } else {
121        Ok(ScrollResult::performed(true))
122    }
123}
124
125/// Verify that a scroll operation was successful.
126///
127/// # Arguments
128///
129/// * `adapter` - The engine adapter to use
130/// * `selector` - The CSS selector for the element
131/// * `options` - Scroll options
132///
133/// # Returns
134///
135/// The verification result
136pub async fn verify_scroll(
137    adapter: &dyn EngineAdapter,
138    selector: &str,
139    options: &ScrollOptions,
140) -> Result<ScrollVerificationResult, EngineError> {
141    let start_time = Instant::now();
142    let mut attempts = 0u32;
143
144    while start_time.elapsed() < options.verification_timeout {
145        attempts += 1;
146
147        // Check if element is visible (indicating it's in viewport)
148        let is_visible = adapter.is_visible(selector).await?;
149
150        if is_visible {
151            return Ok(ScrollVerificationResult {
152                verified: true,
153                in_viewport: true,
154                attempts,
155            });
156        }
157
158        tokio::time::sleep(options.verification_retry_interval).await;
159    }
160
161    Ok(ScrollVerificationResult {
162        verified: false,
163        in_viewport: false,
164        attempts,
165    })
166}
167
168/// Scroll element into view only if needed.
169///
170/// This function first checks if the element is already in view
171/// before performing the scroll.
172///
173/// # Arguments
174///
175/// * `adapter` - The engine adapter to use
176/// * `selector` - The CSS selector for the element
177/// * `options` - Scroll options
178///
179/// # Returns
180///
181/// The result of the scroll operation
182pub async fn scroll_into_view_if_needed(
183    adapter: &dyn EngineAdapter,
184    selector: &str,
185    options: &ScrollOptions,
186) -> Result<ScrollResult, EngineError> {
187    // Check if element is already visible
188    let is_visible = adapter.is_visible(selector).await?;
189
190    if is_visible {
191        // Element is already in view, skip scrolling
192        return Ok(ScrollResult::skipped());
193    }
194
195    // Perform scroll
196    let result = scroll_into_view(adapter, selector, options).await?;
197
198    // Wait for scroll animation if needed
199    if result.scrolled && options.behavior == ScrollBehavior::Smooth {
200        tokio::time::sleep(options.wait_after_scroll).await;
201    }
202
203    Ok(result)
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn scroll_behavior_display() {
212        assert_eq!(ScrollBehavior::Smooth.to_string(), "smooth");
213        assert_eq!(ScrollBehavior::Instant.to_string(), "instant");
214    }
215
216    #[test]
217    fn scroll_options_default() {
218        let options = ScrollOptions::default();
219        assert_eq!(options.behavior, ScrollBehavior::Smooth);
220        assert!(options.verify);
221        assert_eq!(options.threshold_percent, 10.0);
222    }
223
224    #[test]
225    fn scroll_result_skipped() {
226        let result = ScrollResult::skipped();
227        assert!(!result.scrolled);
228        assert!(result.verified);
229        assert!(result.skipped);
230    }
231
232    #[test]
233    fn scroll_result_performed() {
234        let result = ScrollResult::performed(true);
235        assert!(result.scrolled);
236        assert!(result.verified);
237        assert!(!result.skipped);
238
239        let result = ScrollResult::performed(false);
240        assert!(result.scrolled);
241        assert!(!result.verified);
242        assert!(!result.skipped);
243    }
244
245    #[test]
246    fn scroll_result_failed() {
247        let result = ScrollResult::failed();
248        assert!(!result.scrolled);
249        assert!(!result.verified);
250        assert!(!result.skipped);
251    }
252}