browser_use/browser/
session.rs

1use crate::browser::config::{ConnectionOptions, LaunchOptions};
2use crate::dom::DomTree;
3use crate::error::{BrowserError, Result};
4use crate::tools::{ToolContext, ToolRegistry};
5use headless_chrome::{Browser, Tab};
6use std::ffi::OsStr;
7use std::sync::Arc;
8use std::time::Duration;
9
10/// Wrapper for Tab and Element to maintain proper lifetime relationships
11pub struct TabElement<'a> {
12    pub tab: Arc<Tab>,
13    pub element: headless_chrome::Element<'a>,
14}
15
16/// Browser session that manages a Chrome/Chromium instance
17pub struct BrowserSession {
18    /// The underlying headless_chrome Browser instance
19    browser: Browser,
20
21    /// Tool registry for executing browser automation tools
22    tool_registry: ToolRegistry,
23}
24
25impl BrowserSession {
26    /// Launch a new browser instance with the given options
27    pub fn launch(options: LaunchOptions) -> Result<Self> {
28        let mut launch_opts = headless_chrome::LaunchOptions::default();
29
30        // Ignore default arguments to prevent detection by anti-bot services
31        launch_opts
32            .ignore_default_args
33            .push(OsStr::new("--enable-automation"));
34        launch_opts
35            .args
36            .push(OsStr::new("--disable-blink-features=AutomationControlled"));
37
38        // Set the browser's idle timeout to 1 hour (default is 30 seconds) to prevent the session from closing too soon
39        launch_opts.idle_browser_timeout = Duration::from_secs(60 * 60);
40
41        // Configure headless mode
42        launch_opts.headless = options.headless;
43
44        // Set window size
45        launch_opts.window_size = Some((options.window_width, options.window_height));
46
47        // Set Chrome binary path if provided
48        if let Some(path) = options.chrome_path {
49            launch_opts.path = Some(path);
50        }
51
52        // Set user data directory if provided
53        if let Some(dir) = options.user_data_dir {
54            launch_opts.user_data_dir = Some(dir);
55        }
56
57        // Set sandbox mode
58        launch_opts.sandbox = options.sandbox;
59
60        // Launch browser
61        let browser =
62            Browser::new(launch_opts).map_err(|e| BrowserError::LaunchFailed(e.to_string()))?;
63
64        browser
65            .new_tab()
66            .map_err(|e| BrowserError::LaunchFailed(format!("Failed to create tab: {}", e)))?;
67
68        Ok(Self {
69            browser,
70            tool_registry: ToolRegistry::with_defaults(),
71        })
72    }
73
74    /// Connect to an existing browser instance via WebSocket
75    pub fn connect(options: ConnectionOptions) -> Result<Self> {
76        let browser = Browser::connect(options.ws_url)
77            .map_err(|e| BrowserError::ConnectionFailed(e.to_string()))?;
78
79        Ok(Self {
80            browser,
81            tool_registry: ToolRegistry::with_defaults(),
82        })
83    }
84
85    /// Launch a browser with default options
86    pub fn new() -> Result<Self> {
87        Self::launch(LaunchOptions::default())
88    }
89
90    /// Get the active tab
91    pub fn tab(&self) -> Result<Arc<Tab>> {
92        self.get_active_tab()
93    }
94
95    /// Create a new tab and set it as active
96    pub fn new_tab(&mut self) -> Result<Arc<Tab>> {
97        let tab = self.browser.new_tab().map_err(|e| {
98            BrowserError::TabOperationFailed(format!("Failed to create tab: {}", e))
99        })?;
100        Ok(tab)
101    }
102
103    /// Get all tabs
104    pub fn get_tabs(&self) -> Result<Vec<Arc<Tab>>> {
105        let tabs = self
106            .browser
107            .get_tabs()
108            .lock()
109            .map_err(|e| BrowserError::TabOperationFailed(format!("Failed to get tabs: {}", e)))?
110            .clone();
111
112        Ok(tabs)
113    }
114
115    /// Get the currently active tab by checking the document visibility and focus state
116    pub fn get_active_tab(&self) -> Result<Arc<Tab>> {
117        let tabs = self.get_tabs()?;
118
119        // First pass: check for both visibility and focus (strongest signal)
120        for tab in &tabs {
121            let result = tab.evaluate(
122                "document.visibilityState === 'visible' && document.hasFocus()",
123                false,
124            );
125            match result {
126                Ok(remote_object) => {
127                    if let Some(value) = remote_object.value {
128                        if value.as_bool().unwrap_or(false) {
129                            return Ok(tab.clone());
130                        }
131                    }
132                }
133                Err(e) => {
134                    log::debug!("Failed to check tab status: {}", e);
135                    continue;
136                }
137            }
138        }
139
140        // Second pass: check just for visibility (weaker signal, but better than nothing)
141        for tab in &tabs {
142            let result = tab.evaluate("document.visibilityState === 'visible'", false);
143            match result {
144                Ok(remote_object) => {
145                    if let Some(value) = remote_object.value {
146                        if value.as_bool().unwrap_or(false) {
147                            return Ok(tab.clone());
148                        }
149                    }
150                }
151                Err(_) => continue,
152            }
153        }
154
155        Err(BrowserError::TabOperationFailed(
156            "No active tab found".to_string(),
157        ))
158    }
159
160    /// Close the active tab
161    pub fn close_active_tab(&mut self) -> Result<()> {
162        self.tab()?
163            .close(true)
164            .map_err(|e| BrowserError::TabOperationFailed(format!("Failed to close tab: {}", e)))?;
165
166        Ok(())
167    }
168
169    /// Get the underlying Browser instance
170    pub fn browser(&self) -> &Browser {
171        &self.browser
172    }
173
174    /// Navigate to a URL using the active tab
175    pub fn navigate(&self, url: &str) -> Result<()> {
176        self.tab()?.navigate_to(url).map_err(|e| {
177            BrowserError::NavigationFailed(format!("Failed to navigate to {}: {}", url, e))
178        })?;
179
180        Ok(())
181    }
182
183    /// Wait for navigation to complete
184    pub fn wait_for_navigation(&self) -> Result<()> {
185        self.tab()?
186            .wait_until_navigated()
187            .map_err(|e| BrowserError::NavigationFailed(format!("Navigation timeout: {}", e)))?;
188
189        Ok(())
190    }
191
192    /// Extract the DOM tree from the active tab
193    pub fn extract_dom(&self) -> Result<DomTree> {
194        DomTree::from_tab(&self.tab()?)
195    }
196
197    /// Extract the DOM tree with a custom ref prefix (for iframe handling)
198    pub fn extract_dom_with_prefix(&self, prefix: &str) -> Result<DomTree> {
199        DomTree::from_tab_with_prefix(&self.tab()?, prefix)
200    }
201
202    /// Find an element by CSS selector using the provided tab
203    pub fn find_element<'a>(
204        &self,
205        tab: &'a Arc<Tab>,
206        css_selector: &str,
207    ) -> Result<headless_chrome::Element<'a>> {
208        tab.find_element(css_selector).map_err(|e| {
209            BrowserError::ElementNotFound(format!("Element '{}' not found: {}", css_selector, e))
210        })
211    }
212
213    /// Get the tool registry
214    pub fn tool_registry(&self) -> &ToolRegistry {
215        &self.tool_registry
216    }
217
218    /// Get mutable tool registry
219    pub fn tool_registry_mut(&mut self) -> &mut ToolRegistry {
220        &mut self.tool_registry
221    }
222
223    /// Execute a tool by name
224    pub fn execute_tool(
225        &self,
226        name: &str,
227        params: serde_json::Value,
228    ) -> Result<crate::tools::ToolResult> {
229        let mut context = ToolContext::new(self);
230        self.tool_registry.execute(name, params, &mut context)
231    }
232
233    /// Navigate back in browser history
234    pub fn go_back(&self) -> Result<()> {
235        let go_back_js = r#"
236            (function() {
237                window.history.back();
238                return true;
239            })()
240        "#;
241
242        self.tab()?
243            .evaluate(go_back_js, false)
244            .map_err(|e| BrowserError::NavigationFailed(format!("Failed to go back: {}", e)))?;
245
246        // Wait a moment for navigation
247        std::thread::sleep(std::time::Duration::from_millis(300));
248
249        Ok(())
250    }
251
252    /// Navigate forward in browser history
253    pub fn go_forward(&self) -> Result<()> {
254        let go_forward_js = r#"
255            (function() {
256                window.history.forward();
257                return true;
258            })()
259        "#;
260
261        self.tab()?
262            .evaluate(go_forward_js, false)
263            .map_err(|e| BrowserError::NavigationFailed(format!("Failed to go forward: {}", e)))?;
264
265        // Wait a moment for navigation
266        std::thread::sleep(std::time::Duration::from_millis(300));
267
268        Ok(())
269    }
270
271    /// Close the browser
272    pub fn close(&self) -> Result<()> {
273        // Note: The Browser struct doesn't have a public close method in headless_chrome
274        // The browser will be closed when the Browser instance is dropped
275        // We can close all tabs to effectively shut down
276        let tabs = self.get_tabs()?;
277        for tab in tabs {
278            let _ = tab.close(false); // Ignore errors on individual tab closes
279        }
280        Ok(())
281    }
282}
283
284impl Default for BrowserSession {
285    fn default() -> Self {
286        Self::new().expect("Failed to create default browser session")
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_launch_options_builder() {
296        let opts = LaunchOptions::new().headless(true).window_size(800, 600);
297
298        assert!(opts.headless);
299        assert_eq!(opts.window_width, 800);
300        assert_eq!(opts.window_height, 600);
301    }
302
303    #[test]
304    fn test_connection_options() {
305        let opts = ConnectionOptions::new("ws://localhost:9222").timeout(5000);
306
307        assert_eq!(opts.ws_url, "ws://localhost:9222");
308        assert_eq!(opts.timeout, 5000);
309    }
310
311    #[test]
312    #[ignore]
313    fn test_get_active_tab() {
314        let session = BrowserSession::launch(LaunchOptions::new().headless(true))
315            .expect("Failed to launch browser");
316
317        let tab = session.get_active_tab();
318        assert!(tab.is_ok());
319    }
320
321    // Integration tests (require Chrome to be installed)
322    #[test]
323    #[ignore] // Ignore by default, run with: cargo test -- --ignored
324    fn test_launch_browser() {
325        let result = BrowserSession::launch(LaunchOptions::new().headless(true));
326        assert!(result.is_ok());
327    }
328
329    #[test]
330    #[ignore]
331    fn test_navigate() {
332        let session = BrowserSession::launch(LaunchOptions::new().headless(true))
333            .expect("Failed to launch browser");
334
335        let result = session.navigate("about:blank");
336        assert!(result.is_ok());
337    }
338
339    #[test]
340    #[ignore]
341    fn test_new_tab() {
342        let mut session = BrowserSession::launch(LaunchOptions::new().headless(true))
343            .expect("Failed to launch browser");
344
345        let result = session.new_tab();
346        assert!(result.is_ok());
347
348        let tabs = session.get_tabs().expect("Failed to get tabs");
349        assert!(tabs.len() >= 2);
350    }
351}