//! Browser Automation Manager //! //! Handles browser lifecycle, navigation, and interaction for UX testing use thirtyfour::prelude::*; use std::path::PathBuf; use std::time::Duration; use tokio::time::timeout; /// Supported browser types for testing #[derive(Debug, Clone)] pub enum BrowserType { Chrome, Firefox, Safari, Edge, } /// Browser manager for UX testing pub struct BrowserManager { driver: WebDriver, base_url: String, screenshot_dir: PathBuf, config: super::UXTestConfig, } impl Clone for BrowserManager { fn clone(&self) -> Self { // Note: WebDriver cannot be cloned, so this creates a reference to the same driver // In practice, we should avoid cloning BrowserManager and use references instead panic!("BrowserManager cannot be cloned due to WebDriver limitations. Use references instead."); } } impl BrowserManager { /// Create a new browser manager pub async fn new(config: &super::UXTestConfig) -> Result> { let capabilities = match config.browser_type { BrowserType::Chrome => { let mut caps = DesiredCapabilities::chrome(); if config.headless { caps.add_chrome_arg("--headless")?; } caps.add_chrome_arg("--no-sandbox")?; caps.add_chrome_arg("--disable-dev-shm-usage")?; caps.add_chrome_arg("--disable-gpu")?; caps.add_chrome_arg("--window-size=1920,1080")?; caps } BrowserType::Firefox => { let mut caps = DesiredCapabilities::firefox(); if config.headless { caps.add_firefox_arg("--headless")?; } caps } _ => { return Err("Browser type not yet implemented".into()); } }; // Try to connect to existing WebDriver or start one let driver = match WebDriver::new("http://localhost:4444", capabilities.clone()).await { Ok(driver) => driver, Err(_) => { // If selenium server is not running, try local driver WebDriver::new("http://localhost:9515", capabilities).await? } }; // Configure browser driver.set_window_size(1920, 1080).await?; driver.implicitly_wait(Duration::from_secs(10)).await?; Ok(Self { driver, base_url: format!("http://localhost:{}", config.test_port), screenshot_dir: config.screenshot_dir.clone(), config: config.clone(), }) } /// Navigate to a path on the test server pub async fn navigate_to(&self, path: &str) -> Result<(), Box> { let url = if path.starts_with("http") { path.to_string() } else { format!("{}{}", self.base_url, path) }; log::info!("Navigating to: {}", url); timeout( Duration::from_secs(self.config.timeout_seconds), self.driver.goto(&url) ).await??; // Wait for page to load self.wait_for_page_load().await?; Ok(()) } /// Wait for page to be fully loaded pub async fn wait_for_page_load(&self) -> Result<(), Box> { // Wait for document ready state self.driver.execute("return document.readyState", vec![]).await?; // Additional wait for any dynamic content tokio::time::sleep(Duration::from_millis(500)).await; Ok(()) } /// Take a screenshot with the given name pub async fn take_screenshot(&self, name: &str) -> Result> { let screenshot = self.driver.screenshot_as_png().await?; let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S"); let filename = format!("{}_{}.png", name, timestamp); let path = self.screenshot_dir.join(filename); std::fs::create_dir_all(&self.screenshot_dir)?; std::fs::write(&path, screenshot)?; log::info!("Screenshot saved: {:?}", path); Ok(path) } /// Find element by CSS selector pub async fn find_element(&self, selector: &str) -> Result> { Ok(self.driver.find(By::Css(selector)).await?) } /// Find elements by CSS selector pub async fn find_elements(&self, selector: &str) -> Result, Box> { Ok(self.driver.find_all(By::Css(selector)).await?) } /// Click element by CSS selector pub async fn click(&self, selector: &str) -> Result<(), Box> { let element = self.find_element(selector).await?; element.scroll_into_view().await?; element.click().await?; Ok(()) } /// Type text into element pub async fn type_text(&self, selector: &str, text: &str) -> Result<(), Box> { let element = self.find_element(selector).await?; element.clear().await?; element.send_keys(text).await?; Ok(()) } /// Get text from element pub async fn get_text(&self, selector: &str) -> Result> { let element = self.find_element(selector).await?; Ok(element.text().await?) } /// Check if element exists pub async fn element_exists(&self, selector: &str) -> bool { self.driver.find(By::Css(selector)).await.is_ok() } /// Wait for element to be visible pub async fn wait_for_element(&self, selector: &str) -> Result> { let timeout_duration = Duration::from_secs(self.config.timeout_seconds); timeout(timeout_duration, async { loop { if let Ok(element) = self.driver.find(By::Css(selector)).await { if element.is_displayed().await.unwrap_or(false) { return Ok(element); } } tokio::time::sleep(Duration::from_millis(100)).await; } }).await? } /// Get current page title pub async fn get_title(&self) -> Result> { Ok(self.driver.title().await?) } /// Get current URL pub async fn get_current_url(&self) -> Result> { Ok(self.driver.current_url().await?) } /// Execute JavaScript pub async fn execute_script(&self, script: &str) -> Result> { Ok(self.driver.execute(script, vec![]).await?) } /// Quit the browser pub async fn quit(&mut self) -> Result<(), Box> { self.driver.quit().await?; Ok(()) } /// Get page source for debugging pub async fn get_page_source(&self) -> Result> { Ok(self.driver.source().await?) } /// Scroll to element pub async fn scroll_to_element(&self, selector: &str) -> Result<(), Box> { let element = self.find_element(selector).await?; element.scroll_into_view().await?; Ok(()) } /// Wait for text to appear in element pub async fn wait_for_text(&self, selector: &str, expected_text: &str) -> Result<(), Box> { let timeout_duration = Duration::from_secs(self.config.timeout_seconds); timeout(timeout_duration, async { loop { if let Ok(element) = self.driver.find(By::Css(selector)).await { if let Ok(text) = element.text().await { if text.contains(expected_text) { return Ok(()); } } } tokio::time::sleep(Duration::from_millis(100)).await; } }).await? } }