browser_commander/interactions/
scroll.rs1use crate::core::constants::TIMING;
7use crate::core::engine::{EngineAdapter, EngineError, ScrollVerificationResult};
8use std::time::{Duration, Instant};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum ScrollBehavior {
13 #[default]
15 Smooth,
16 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#[derive(Debug, Clone)]
31pub struct ScrollOptions {
32 pub behavior: ScrollBehavior,
34 pub verify: bool,
36 pub verification_timeout: Duration,
38 pub verification_retry_interval: Duration,
40 pub threshold_percent: f64,
42 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#[derive(Debug, Clone)]
61pub struct ScrollResult {
62 pub scrolled: bool,
64 pub verified: bool,
66 pub skipped: bool,
68}
69
70impl ScrollResult {
71 pub fn skipped() -> Self {
73 Self {
74 scrolled: false,
75 verified: true,
76 skipped: true,
77 }
78 }
79
80 pub fn performed(verified: bool) -> Self {
82 Self {
83 scrolled: true,
84 verified,
85 skipped: false,
86 }
87 }
88
89 pub fn failed() -> Self {
91 Self {
92 scrolled: false,
93 verified: false,
94 skipped: false,
95 }
96 }
97}
98
99pub 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
125pub 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 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
168pub async fn scroll_into_view_if_needed(
183 adapter: &dyn EngineAdapter,
184 selector: &str,
185 options: &ScrollOptions,
186) -> Result<ScrollResult, EngineError> {
187 let is_visible = adapter.is_visible(selector).await?;
189
190 if is_visible {
191 return Ok(ScrollResult::skipped());
193 }
194
195 let result = scroll_into_view(adapter, selector, options).await?;
197
198 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}