230 lines
7.9 KiB
Rust
230 lines
7.9 KiB
Rust
//! 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<Self, Box<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
// 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<PathBuf, Box<dyn std::error::Error>> {
|
|
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<WebElement, Box<dyn std::error::Error>> {
|
|
Ok(self.driver.find(By::Css(selector)).await?)
|
|
}
|
|
|
|
/// Find elements by CSS selector
|
|
pub async fn find_elements(&self, selector: &str) -> Result<Vec<WebElement>, Box<dyn std::error::Error>> {
|
|
Ok(self.driver.find_all(By::Css(selector)).await?)
|
|
}
|
|
|
|
/// Click element by CSS selector
|
|
pub async fn click(&self, selector: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<String, Box<dyn std::error::Error>> {
|
|
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<WebElement, Box<dyn std::error::Error>> {
|
|
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<String, Box<dyn std::error::Error>> {
|
|
Ok(self.driver.title().await?)
|
|
}
|
|
|
|
/// Get current URL
|
|
pub async fn get_current_url(&self) -> Result<String, Box<dyn std::error::Error>> {
|
|
Ok(self.driver.current_url().await?)
|
|
}
|
|
|
|
/// Execute JavaScript
|
|
pub async fn execute_script(&self, script: &str) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
|
Ok(self.driver.execute(script, vec![]).await?)
|
|
}
|
|
|
|
/// Quit the browser
|
|
pub async fn quit(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
|
self.driver.quit().await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Get page source for debugging
|
|
pub async fn get_page_source(&self) -> Result<String, Box<dyn std::error::Error>> {
|
|
Ok(self.driver.source().await?)
|
|
}
|
|
|
|
/// Scroll to element
|
|
pub async fn scroll_to_element(&self, selector: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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?
|
|
}
|
|
} |