browser_use/tools/
navigate.rs

1use crate::error::Result;
2use crate::tools::{Tool, ToolContext, ToolResult};
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6/// Normalize an incomplete URL by adding missing protocol and handling common patterns
7fn normalize_url(url: &str) -> String {
8    let trimmed = url.trim();
9    
10    // If already has a protocol, return as-is
11    if trimmed.starts_with("http://")
12        || trimmed.starts_with("https://")
13        || trimmed.starts_with("file://")
14        || trimmed.starts_with("data:")
15        || trimmed.starts_with("about:")
16        || trimmed.starts_with("chrome://")
17        || trimmed.starts_with("chrome-extension://")
18    {
19        return trimmed.to_string();
20    }
21
22    // Relative path - return as-is
23    if trimmed.starts_with('/') || trimmed.starts_with("./") || trimmed.starts_with("../") {
24        return trimmed.to_string();
25    }
26
27    // localhost special case - use http by default
28    if trimmed.starts_with("localhost") || trimmed.starts_with("127.0.0.1") {
29        return format!("http://{}", trimmed);
30    }
31
32    // Check if it looks like a domain (contains dot or is a known TLD)
33    if trimmed.contains('.') {
34        // Looks like a domain - add https://
35        return format!("https://{}", trimmed);
36    }
37
38    // Single word - assume it's a domain name, add www. prefix and https://
39    // This handles cases like "google" -> "https://www.google.com"
40    format!("https://www.{}.com", trimmed)
41}
42
43/// Parameters for the navigate tool
44#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
45pub struct NavigateParams {
46    /// URL to navigate to
47    pub url: String,
48
49    /// Wait for navigation to complete (default: true)
50    #[serde(default = "default_wait")]
51    pub wait_for_load: bool,
52}
53
54fn default_wait() -> bool {
55    true
56}
57
58/// Tool for navigating to a URL
59#[derive(Default)]
60pub struct NavigateTool;
61
62impl Tool for NavigateTool {
63    type Params = NavigateParams;
64
65    fn name(&self) -> &str {
66        "navigate"
67    }
68
69    fn execute_typed(
70        &self,
71        params: NavigateParams,
72        context: &mut ToolContext,
73    ) -> Result<ToolResult> {
74        // Normalize the URL
75        let normalized_url = normalize_url(&params.url);
76        
77        // Navigate to normalized URL
78        context.session.navigate(&normalized_url)?;
79
80        // Wait for navigation if requested
81        if params.wait_for_load {
82            context.session.wait_for_navigation()?;
83        }
84
85        Ok(ToolResult::success_with(serde_json::json!({
86            "original_url": params.url,
87            "normalized_url": normalized_url,
88            "waited": params.wait_for_load
89        })))
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn test_navigate_params_default() {
99        let json = serde_json::json!({
100            "url": "https://example.com"
101        });
102
103        let params: NavigateParams = serde_json::from_value(json).unwrap();
104        assert_eq!(params.url, "https://example.com");
105        assert!(params.wait_for_load);
106    }
107
108    #[test]
109    fn test_navigate_params_explicit_wait() {
110        let json = serde_json::json!({
111            "url": "https://example.com",
112            "wait_for_load": false
113        });
114
115        let params: NavigateParams = serde_json::from_value(json).unwrap();
116        assert_eq!(params.url, "https://example.com");
117        assert!(!params.wait_for_load);
118    }
119
120    #[test]
121    fn test_navigate_tool_metadata() {
122        let tool = NavigateTool;
123        assert_eq!(tool.name(), "navigate");
124        let schema = tool.parameters_schema();
125        assert!(schema.is_object());
126    }
127
128    #[test]
129    fn test_normalize_url_complete() {
130        assert_eq!(normalize_url("https://example.com"), "https://example.com");
131        assert_eq!(normalize_url("http://example.com"), "http://example.com");
132        assert_eq!(
133            normalize_url("https://example.com/path"),
134            "https://example.com/path"
135        );
136    }
137
138    #[test]
139    fn test_normalize_url_missing_protocol() {
140        assert_eq!(normalize_url("example.com"), "https://example.com");
141        assert_eq!(
142            normalize_url("example.com/path"),
143            "https://example.com/path"
144        );
145        assert_eq!(
146            normalize_url("sub.example.com"),
147            "https://sub.example.com"
148        );
149    }
150
151    #[test]
152    fn test_normalize_url_partial_domain() {
153        assert_eq!(normalize_url("google"), "https://www.google.com");
154        assert_eq!(normalize_url("github"), "https://www.github.com");
155        assert_eq!(normalize_url("amazon"), "https://www.amazon.com");
156    }
157
158    #[test]
159    fn test_normalize_url_localhost() {
160        assert_eq!(normalize_url("localhost"), "http://localhost");
161        assert_eq!(normalize_url("localhost:3000"), "http://localhost:3000");
162        assert_eq!(normalize_url("127.0.0.1"), "http://127.0.0.1");
163        assert_eq!(normalize_url("127.0.0.1:8080"), "http://127.0.0.1:8080");
164    }
165
166    #[test]
167    fn test_normalize_url_special_protocols() {
168        assert_eq!(normalize_url("about:blank"), "about:blank");
169        assert_eq!(normalize_url("file:///path/to/file"), "file:///path/to/file");
170        assert_eq!(
171            normalize_url("data:text/html,<h1>Test</h1>"),
172            "data:text/html,<h1>Test</h1>"
173        );
174        assert_eq!(normalize_url("chrome://settings"), "chrome://settings");
175    }
176
177    #[test]
178    fn test_normalize_url_relative_paths() {
179        assert_eq!(normalize_url("/path"), "/path");
180        assert_eq!(normalize_url("/path/to/page"), "/path/to/page");
181        assert_eq!(normalize_url("./relative"), "./relative");
182        assert_eq!(normalize_url("../parent"), "../parent");
183    }
184
185    #[test]
186    fn test_normalize_url_whitespace() {
187        assert_eq!(normalize_url("  example.com  "), "https://example.com");
188        assert_eq!(
189            normalize_url("  https://example.com  "),
190            "https://example.com"
191        );
192    }
193}