use crate::core::constants::TIMING;
use crate::core::engine::{EngineAdapter, EngineError, ScrollVerificationResult};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ScrollBehavior {
#[default]
Smooth,
Instant,
}
impl std::fmt::Display for ScrollBehavior {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ScrollBehavior::Smooth => write!(f, "smooth"),
ScrollBehavior::Instant => write!(f, "instant"),
}
}
}
#[derive(Debug, Clone)]
pub struct ScrollOptions {
pub behavior: ScrollBehavior,
pub verify: bool,
pub verification_timeout: Duration,
pub verification_retry_interval: Duration,
pub threshold_percent: f64,
pub wait_after_scroll: Duration,
}
impl Default for ScrollOptions {
fn default() -> Self {
Self {
behavior: ScrollBehavior::Smooth,
verify: true,
verification_timeout: TIMING.verification_timeout,
verification_retry_interval: TIMING.verification_retry_interval,
threshold_percent: 10.0,
wait_after_scroll: TIMING.scroll_animation_wait,
}
}
}
#[derive(Debug, Clone)]
pub struct ScrollResult {
pub scrolled: bool,
pub verified: bool,
pub skipped: bool,
}
impl ScrollResult {
pub fn skipped() -> Self {
Self {
scrolled: false,
verified: true,
skipped: true,
}
}
pub fn performed(verified: bool) -> Self {
Self {
scrolled: true,
verified,
skipped: false,
}
}
pub fn failed() -> Self {
Self {
scrolled: false,
verified: false,
skipped: false,
}
}
}
pub async fn scroll_into_view(
adapter: &dyn EngineAdapter,
selector: &str,
options: &ScrollOptions,
) -> Result<ScrollResult, EngineError> {
adapter.scroll_into_view(selector).await?;
if options.verify {
let verification = verify_scroll(adapter, selector, options).await?;
Ok(ScrollResult::performed(verification.verified))
} else {
Ok(ScrollResult::performed(true))
}
}
pub async fn verify_scroll(
adapter: &dyn EngineAdapter,
selector: &str,
options: &ScrollOptions,
) -> Result<ScrollVerificationResult, EngineError> {
let start_time = Instant::now();
let mut attempts = 0u32;
while start_time.elapsed() < options.verification_timeout {
attempts += 1;
let is_visible = adapter.is_visible(selector).await?;
if is_visible {
return Ok(ScrollVerificationResult {
verified: true,
in_viewport: true,
attempts,
});
}
tokio::time::sleep(options.verification_retry_interval).await;
}
Ok(ScrollVerificationResult {
verified: false,
in_viewport: false,
attempts,
})
}
pub async fn scroll_into_view_if_needed(
adapter: &dyn EngineAdapter,
selector: &str,
options: &ScrollOptions,
) -> Result<ScrollResult, EngineError> {
let is_visible = adapter.is_visible(selector).await?;
if is_visible {
return Ok(ScrollResult::skipped());
}
let result = scroll_into_view(adapter, selector, options).await?;
if result.scrolled && options.behavior == ScrollBehavior::Smooth {
tokio::time::sleep(options.wait_after_scroll).await;
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scroll_behavior_display() {
assert_eq!(ScrollBehavior::Smooth.to_string(), "smooth");
assert_eq!(ScrollBehavior::Instant.to_string(), "instant");
}
#[test]
fn scroll_options_default() {
let options = ScrollOptions::default();
assert_eq!(options.behavior, ScrollBehavior::Smooth);
assert!(options.verify);
assert_eq!(options.threshold_percent, 10.0);
}
#[test]
fn scroll_result_skipped() {
let result = ScrollResult::skipped();
assert!(!result.scrolled);
assert!(result.verified);
assert!(result.skipped);
}
#[test]
fn scroll_result_performed() {
let result = ScrollResult::performed(true);
assert!(result.scrolled);
assert!(result.verified);
assert!(!result.skipped);
let result = ScrollResult::performed(false);
assert!(result.scrolled);
assert!(!result.verified);
assert!(!result.skipped);
}
#[test]
fn scroll_result_failed() {
let result = ScrollResult::failed();
assert!(!result.scrolled);
assert!(!result.verified);
assert!(!result.skipped);
}
}