use async_trait::async_trait; use serde_json::Value; /// Abstract backend for browser automation. CDP (Chromium) and WebDriver /// (Safari/iOS) share this interface so actions.rs can remain backend-agnostic /// in the future. #[async_trait] pub trait BrowserBackend: Send + Sync { async fn navigate(&self, url: &str) -> Result<(), String>; async fn get_url(&self) -> Result; async fn get_title(&self) -> Result; async fn get_content(&self) -> Result; async fn evaluate(&self, script: &str) -> Result; async fn screenshot(&self) -> Result; async fn click(&self, selector: &str) -> Result<(), String>; async fn fill(&self, selector: &str, value: &str) -> Result<(), String>; async fn close(&mut self) -> Result<(), String>; async fn back(&self) -> Result<(), String>; async fn forward(&self) -> Result<(), String>; async fn reload(&self) -> Result<(), String>; async fn get_cookies(&self) -> Result; fn backend_type(&self) -> &str; fn supports(&self, feature: &str) -> bool { match feature { "navigate" | "evaluate" | "screenshot" | "click" | "fill" => true, "screencast" | "tracing" | "network_intercept" | "cdp" => self.backend_type() == "cdp", _ => false, } } fn unsupported_error(&self, action: &str) -> String { format!( "Action '{}' is not supported on the {} backend", action, self.backend_type() ) } } /// WebDriver implementation of BrowserBackend pub struct WebDriverBackend { client: super::client::WebDriverClient, } impl WebDriverBackend { pub fn new(client: super::client::WebDriverClient) -> Self { Self { client } } } #[async_trait] impl BrowserBackend for WebDriverBackend { async fn navigate(&self, url: &str) -> Result<(), String> { self.client.navigate(url).await } async fn get_url(&self) -> Result { self.client.get_url().await } async fn get_title(&self) -> Result { self.client.get_title().await } async fn get_content(&self) -> Result { self.client.get_page_source().await } async fn evaluate(&self, script: &str) -> Result { self.client.execute_script(script, vec![]).await } async fn screenshot(&self) -> Result { self.client.screenshot().await } async fn click(&self, selector: &str) -> Result<(), String> { let element_id = self.client.find_element("css selector", selector).await?; self.client.click_element(&element_id).await } async fn fill(&self, selector: &str, value: &str) -> Result<(), String> { let element_id = self.client.find_element("css selector", selector).await?; self.client.clear_element(&element_id).await?; self.client.send_keys(&element_id, value).await } async fn close(&mut self) -> Result<(), String> { self.client.delete_session().await } async fn back(&self) -> Result<(), String> { self.client.back().await } async fn forward(&self) -> Result<(), String> { self.client.forward().await } async fn reload(&self) -> Result<(), String> { self.client.refresh().await } async fn get_cookies(&self) -> Result { self.client.get_cookies().await } fn backend_type(&self) -> &str { "webdriver" } } /// CDP-backed backend constants for unsupported actions on WebDriver pub const WEBDRIVER_UNSUPPORTED_ACTIONS: &[&str] = &[ "screencast_start", "screencast_stop", "trace_start", "trace_stop", "profiler_start", "profiler_stop", "route", "unroute", "expose", "addscript", "addinitscript", "network", "har_start", "har_stop", ]; #[cfg(test)] mod tests { use super::*; #[test] fn test_unsupported_actions() { assert!(WEBDRIVER_UNSUPPORTED_ACTIONS.contains(&"screencast_start")); assert!(WEBDRIVER_UNSUPPORTED_ACTIONS.contains(&"trace_start")); assert!(!WEBDRIVER_UNSUPPORTED_ACTIONS.contains(&"navigate")); } }