use crate::browser::config::{ConnectionOptions, LaunchOptions};
use crate::dom::DomTree;
use crate::error::{BrowserError, Result};
use crate::tools::{ToolContext, ToolRegistry};
use headless_chrome::{Browser, Tab};
use std::ffi::OsStr;
use std::sync::Arc;
use std::time::Duration;
pub struct TabElement<'a> {
pub tab: Arc<Tab>,
pub element: headless_chrome::Element<'a>,
}
pub struct BrowserSession {
browser: Browser,
tool_registry: ToolRegistry,
}
impl BrowserSession {
pub fn launch(options: LaunchOptions) -> Result<Self> {
let mut launch_opts = headless_chrome::LaunchOptions::default();
launch_opts
.ignore_default_args
.push(OsStr::new("--enable-automation"));
launch_opts
.args
.push(OsStr::new("--disable-blink-features=AutomationControlled"));
launch_opts.idle_browser_timeout = Duration::from_secs(60 * 60);
launch_opts.headless = options.headless;
launch_opts.window_size = Some((options.window_width, options.window_height));
if let Some(path) = options.chrome_path {
launch_opts.path = Some(path);
}
if let Some(dir) = options.user_data_dir {
launch_opts.user_data_dir = Some(dir);
}
launch_opts.sandbox = options.sandbox;
let browser =
Browser::new(launch_opts).map_err(|e| BrowserError::LaunchFailed(e.to_string()))?;
browser
.new_tab()
.map_err(|e| BrowserError::LaunchFailed(format!("Failed to create tab: {}", e)))?;
Ok(Self {
browser,
tool_registry: ToolRegistry::with_defaults(),
})
}
pub fn connect(options: ConnectionOptions) -> Result<Self> {
let browser = Browser::connect(options.ws_url)
.map_err(|e| BrowserError::ConnectionFailed(e.to_string()))?;
Ok(Self {
browser,
tool_registry: ToolRegistry::with_defaults(),
})
}
pub fn new() -> Result<Self> {
Self::launch(LaunchOptions::default())
}
pub fn tab(&self) -> Result<Arc<Tab>> {
self.get_active_tab()
}
pub fn new_tab(&mut self) -> Result<Arc<Tab>> {
let tab = self.browser.new_tab().map_err(|e| {
BrowserError::TabOperationFailed(format!("Failed to create tab: {}", e))
})?;
Ok(tab)
}
pub fn get_tabs(&self) -> Result<Vec<Arc<Tab>>> {
let tabs = self
.browser
.get_tabs()
.lock()
.map_err(|e| BrowserError::TabOperationFailed(format!("Failed to get tabs: {}", e)))?
.clone();
Ok(tabs)
}
pub fn get_active_tab(&self) -> Result<Arc<Tab>> {
let tabs = self.get_tabs()?;
for tab in &tabs {
let result = tab.evaluate(
"document.visibilityState === 'visible' && document.hasFocus()",
false,
);
match result {
Ok(remote_object) => {
if let Some(value) = remote_object.value {
if value.as_bool().unwrap_or(false) {
return Ok(tab.clone());
}
}
}
Err(e) => {
log::debug!("Failed to check tab status: {}", e);
continue;
}
}
}
for tab in &tabs {
let result = tab.evaluate("document.visibilityState === 'visible'", false);
match result {
Ok(remote_object) => {
if let Some(value) = remote_object.value {
if value.as_bool().unwrap_or(false) {
return Ok(tab.clone());
}
}
}
Err(_) => continue,
}
}
Err(BrowserError::TabOperationFailed(
"No active tab found".to_string(),
))
}
pub fn close_active_tab(&mut self) -> Result<()> {
self.tab()?
.close(true)
.map_err(|e| BrowserError::TabOperationFailed(format!("Failed to close tab: {}", e)))?;
Ok(())
}
pub fn browser(&self) -> &Browser {
&self.browser
}
pub fn navigate(&self, url: &str) -> Result<()> {
self.tab()?.navigate_to(url).map_err(|e| {
BrowserError::NavigationFailed(format!("Failed to navigate to {}: {}", url, e))
})?;
Ok(())
}
pub fn wait_for_navigation(&self) -> Result<()> {
self.tab()?
.wait_until_navigated()
.map_err(|e| BrowserError::NavigationFailed(format!("Navigation timeout: {}", e)))?;
Ok(())
}
pub fn extract_dom(&self) -> Result<DomTree> {
DomTree::from_tab(&self.tab()?)
}
pub fn extract_dom_with_prefix(&self, prefix: &str) -> Result<DomTree> {
DomTree::from_tab_with_prefix(&self.tab()?, prefix)
}
pub fn find_element<'a>(
&self,
tab: &'a Arc<Tab>,
css_selector: &str,
) -> Result<headless_chrome::Element<'a>> {
tab.find_element(css_selector).map_err(|e| {
BrowserError::ElementNotFound(format!("Element '{}' not found: {}", css_selector, e))
})
}
pub fn tool_registry(&self) -> &ToolRegistry {
&self.tool_registry
}
pub fn tool_registry_mut(&mut self) -> &mut ToolRegistry {
&mut self.tool_registry
}
pub fn execute_tool(
&self,
name: &str,
params: serde_json::Value,
) -> Result<crate::tools::ToolResult> {
let mut context = ToolContext::new(self);
self.tool_registry.execute(name, params, &mut context)
}
pub fn go_back(&self) -> Result<()> {
let go_back_js = r#"
(function() {
window.history.back();
return true;
})()
"#;
self.tab()?
.evaluate(go_back_js, false)
.map_err(|e| BrowserError::NavigationFailed(format!("Failed to go back: {}", e)))?;
std::thread::sleep(std::time::Duration::from_millis(300));
Ok(())
}
pub fn go_forward(&self) -> Result<()> {
let go_forward_js = r#"
(function() {
window.history.forward();
return true;
})()
"#;
self.tab()?
.evaluate(go_forward_js, false)
.map_err(|e| BrowserError::NavigationFailed(format!("Failed to go forward: {}", e)))?;
std::thread::sleep(std::time::Duration::from_millis(300));
Ok(())
}
pub fn close(&self) -> Result<()> {
let tabs = self.get_tabs()?;
for tab in tabs {
let _ = tab.close(false); }
Ok(())
}
}
impl Default for BrowserSession {
fn default() -> Self {
Self::new().expect("Failed to create default browser session")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_launch_options_builder() {
let opts = LaunchOptions::new().headless(true).window_size(800, 600);
assert!(opts.headless);
assert_eq!(opts.window_width, 800);
assert_eq!(opts.window_height, 600);
}
#[test]
fn test_connection_options() {
let opts = ConnectionOptions::new("ws://localhost:9222").timeout(5000);
assert_eq!(opts.ws_url, "ws://localhost:9222");
assert_eq!(opts.timeout, 5000);
}
#[test]
#[ignore]
fn test_get_active_tab() {
let session = BrowserSession::launch(LaunchOptions::new().headless(true))
.expect("Failed to launch browser");
let tab = session.get_active_tab();
assert!(tab.is_ok());
}
#[test]
#[ignore] fn test_launch_browser() {
let result = BrowserSession::launch(LaunchOptions::new().headless(true));
assert!(result.is_ok());
}
#[test]
#[ignore]
fn test_navigate() {
let session = BrowserSession::launch(LaunchOptions::new().headless(true))
.expect("Failed to launch browser");
let result = session.navigate("about:blank");
assert!(result.is_ok());
}
#[test]
#[ignore]
fn test_new_tab() {
let mut session = BrowserSession::launch(LaunchOptions::new().headless(true))
.expect("Failed to launch browser");
let result = session.new_tab();
assert!(result.is_ok());
let tabs = session.get_tabs().expect("Failed to get tabs");
assert!(tabs.len() >= 2);
}
}