use std::sync::Arc;
use rmcp::ErrorData;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use zendriver::ScrollOptions;
use crate::errors::{McpServerError, map_error};
use crate::state::SessionState;
use crate::tools::common::{current_tab, page_snapshot};
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PageScrollInput {
#[serde(default)]
pub dx: f64,
#[serde(default)]
pub dy: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub speed: Option<i64>,
#[serde(default)]
pub return_snapshot: bool,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct PageScrollOutput {
pub scroll_x: f64,
pub scroll_y: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub snapshot: Option<String>,
}
pub async fn scroll(
state: Arc<Mutex<SessionState>>,
input: PageScrollInput,
) -> Result<PageScrollOutput, ErrorData> {
let s = state.lock().await;
let tab = current_tab(&s).await?;
tab.scroll_with(ScrollOptions {
dx: -input.dx,
dy: -input.dy,
speed: input.speed,
})
.await
.map_err(|e| map_error(McpServerError::from(e)))?;
let (scroll_x, scroll_y): (f64, f64) = tab
.evaluate_main("[window.scrollX, window.scrollY]")
.await
.map_err(|e| map_error(McpServerError::from(e)))?;
let snapshot = if input.return_snapshot {
Some(page_snapshot(&tab).await?)
} else {
None
};
Ok(PageScrollOutput {
scroll_x,
scroll_y,
snapshot,
})
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
#[tokio::test]
async fn scroll_with_no_browser_errors() {
let state = Arc::new(Mutex::new(SessionState::new()));
let err = scroll(
state,
PageScrollInput {
dx: 0.0,
dy: 500.0,
speed: None,
return_snapshot: false,
},
)
.await
.expect_err("expected BrowserNotOpen");
assert!(err.message.contains("Browser not open"));
}
}