browser_use/tools/
scroll.rs

1use crate::error::{BrowserError, Result};
2use crate::tools::{Tool, ToolContext, ToolResult};
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6/// Parameters for the scroll tool
7#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
8pub struct ScrollParams {
9    /// Amount to scroll in pixels (positive for down, negative for up).
10    /// If not provided, scrolls to the bottom of the page.
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub amount: Option<i32>,
13}
14
15/// Tool for scrolling the page
16#[derive(Default)]
17pub struct ScrollTool;
18
19const SCROLL_JS: &str = include_str!("scroll.js");
20
21impl Tool for ScrollTool {
22    type Params = ScrollParams;
23
24    fn name(&self) -> &str {
25        "scroll"
26    }
27
28    fn execute_typed(&self, params: ScrollParams, context: &mut ToolContext) -> Result<ToolResult> {
29        let config = serde_json::json!({
30            "amount": params.amount
31        });
32        let scroll_js = SCROLL_JS.replace("__SCROLL_CONFIG__", &config.to_string());
33
34        let result = context
35            .session
36            .tab()?
37            .evaluate(&scroll_js, true)
38            .map_err(|e| BrowserError::ToolExecutionFailed {
39                tool: "scroll".to_string(),
40                reason: e.to_string(),
41            })?;
42
43        // Parse the JSON string returned by JavaScript
44        let result_json: serde_json::Value =
45            if let Some(serde_json::Value::String(json_str)) = result.value {
46                serde_json::from_str(&json_str)
47                    .unwrap_or(serde_json::json!({"actualScroll": 0, "isAtBottom": false}))
48            } else {
49                result
50                    .value
51                    .unwrap_or(serde_json::json!({"actualScroll": 0, "isAtBottom": false}))
52            };
53
54        let actual_scroll = result_json["actualScroll"].as_i64().unwrap_or(0);
55        let is_at_bottom = result_json["isAtBottom"].as_bool().unwrap_or(false);
56
57        let message = if is_at_bottom {
58            format!(
59                "Scrolled {} pixels. Reached the bottom of the page.",
60                actual_scroll
61            )
62        } else {
63            format!(
64                "Scrolled {} pixels. Did not reach the bottom of the page.",
65                actual_scroll
66            )
67        };
68
69        Ok(ToolResult::success_with(serde_json::json!({
70            "scrolled": actual_scroll,
71            "isAtBottom": is_at_bottom,
72            "message": message
73        })))
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_scroll_params_with_amount() {
83        let json = serde_json::json!({
84            "amount": 500
85        });
86
87        let params: ScrollParams = serde_json::from_value(json).unwrap();
88        assert_eq!(params.amount, Some(500));
89    }
90
91    #[test]
92    fn test_scroll_params_negative_amount() {
93        let json = serde_json::json!({
94            "amount": -300
95        });
96
97        let params: ScrollParams = serde_json::from_value(json).unwrap();
98        assert_eq!(params.amount, Some(-300));
99    }
100
101    #[test]
102    fn test_scroll_params_no_amount() {
103        let json = serde_json::json!({});
104
105        let params: ScrollParams = serde_json::from_value(json).unwrap();
106        assert_eq!(params.amount, None);
107    }
108}