use browsing::browser::cdp::CdpClient;
use browsing::error::BrowsingError;
use std::time::Duration;
const MAX_RETRY_ATTEMPTS: u32 = 3;
#[tokio::test]
async fn test_cdp_retry_logic_transient_error() {
let mut client = CdpClient::new("ws://localhost:9222".to_string());
let result: Result<(), BrowsingError> = client.start().await;
assert!(result.is_err());
if let Err(BrowsingError::Cdp(msg)) = result {
assert!(msg.contains("Failed to connect") || msg.contains("CDP"));
} else {
panic!("Expected CDP error");
}
}
#[tokio::test]
async fn test_cdp_retry_limit_exceeded() {
let client = CdpClient::new("ws://localhost:9999".to_string());
let result: Result<serde_json::Value, BrowsingError> = client
.send_command(
"Page.navigate",
serde_json::json!({"url": "https://example.com"}),
)
.await;
assert!(result.is_err());
match result {
Err(BrowsingError::Cdp(_)) => {
}
Err(BrowsingError::RetryLimitExceeded(attempts, _)) => {
assert!(attempts <= MAX_RETRY_ATTEMPTS + 1);
}
_ => {
panic!("Expected CDP or RetryLimitExceeded error");
}
}
}
#[test]
fn test_backoff_delay_calculation() {
const INITIAL_RETRY_DELAY_MS: u64 = 100;
const MAX_RETRY_DELAY_MS: u64 = 5000;
let delays: Vec<u64> = (0..5)
.map(|attempt| {
let delay = INITIAL_RETRY_DELAY_MS * 2_u64.pow(attempt);
delay.min(MAX_RETRY_DELAY_MS)
})
.collect();
assert_eq!(delays[0], 100);
assert_eq!(delays[1], 200);
assert_eq!(delays[2], 400);
assert_eq!(delays[3], 800);
assert_eq!(delays[4], 1600);
let delay_10 = INITIAL_RETRY_DELAY_MS * 2_u64.pow(10);
assert_eq!(delay_10.min(MAX_RETRY_DELAY_MS), MAX_RETRY_DELAY_MS);
}
#[test]
fn test_retryable_error_classification() {
let retryable_errors = vec![
BrowsingError::Cdp("Failed to send command".to_string()),
BrowsingError::Cdp("No response received".to_string()),
BrowsingError::Cdp("connection lost".to_string()),
BrowsingError::Cdp("WebSocket closed".to_string()),
BrowsingError::Cdp("Target not found".to_string()),
BrowsingError::Cdp("Session not found".to_string()),
];
let client = CdpClient::new("ws://localhost:9222".to_string());
for error in retryable_errors {
assert!(
client.is_retryable_error(&error),
"Error should be retryable: {}",
error
);
}
let non_retryable_errors = vec![
BrowsingError::Config("Invalid config".to_string()),
BrowsingError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "test")),
BrowsingError::Browser("Browser crashed".to_string()),
BrowsingError::Validation("Invalid input".to_string()),
];
for error in non_retryable_errors {
assert!(
!client.is_retryable_error(&error),
"Error should not be retryable: {}",
error
);
}
}
#[test]
fn test_error_display() {
let errors = vec![
BrowsingError::Config("config error".to_string()),
BrowsingError::Cdp("CDP error".to_string()),
BrowsingError::Browser("browser error".to_string()),
BrowsingError::Llm("LLM error".to_string()),
BrowsingError::Agent("agent error".to_string()),
BrowsingError::Dom("DOM error".to_string()),
BrowsingError::Tool("tool error".to_string()),
BrowsingError::Validation("validation error".to_string()),
BrowsingError::RetryLimitExceeded(3, "test error".to_string()),
BrowsingError::ConnectionLost("connection lost".to_string()),
];
for error in errors {
let display = format!("{}", error);
assert!(!display.is_empty());
assert!(display.len() > 10);
}
}
#[test]
fn test_metrics_edge_cases() {
use browsing::metrics::Metrics;
let mut metrics = Metrics::new();
let summary = metrics.summary();
assert!(summary.cdp_avg_time.is_none());
assert!(summary.cdp_success_rate.is_none());
assert!(summary.dom_avg_time.is_none());
assert!(summary.dom_avg_elements.is_none());
assert!(summary.nav_avg_time.is_none());
assert!(summary.screenshot_avg_time.is_none());
metrics.record_cdp_command("test", Duration::from_millis(100), true);
let summary = metrics.summary();
assert_eq!(summary.cdp_avg_time, Some(Duration::from_millis(100)));
assert_eq!(summary.cdp_success_rate, Some(100.0));
let mut fail_metrics = Metrics::new();
fail_metrics.record_cdp_command("test", Duration::from_millis(100), false);
fail_metrics.record_cdp_command("test", Duration::from_millis(100), false);
fail_metrics.record_cdp_command("test", Duration::from_millis(100), false);
let summary = fail_metrics.summary();
assert_eq!(summary.cdp_success_rate, Some(0.0));
}
#[tokio::test]
async fn test_xpath_selector_generation() {
use browsing::dom::serializer::DOMTreeSerializer;
use browsing::dom::views::{EnhancedDOMTreeNode, NodeType};
use std::collections::HashMap;
let mut node = EnhancedDOMTreeNode {
node_id: 1,
backend_node_id: 1,
node_type: NodeType::ElementNode,
node_name: "button".to_string(),
node_value: String::new(),
attributes: HashMap::new(),
is_scrollable: None,
is_visible: None,
absolute_position: None,
target_id: String::new(),
frame_id: None,
session_id: None,
content_document: None,
shadow_root_type: None,
shadow_roots: None,
parent_node: None,
children_nodes: None,
ax_node: None,
snapshot_node: None,
uuid: "test-uuid".to_string(),
};
let serializer = DOMTreeSerializer::new(node.clone());
node.attributes
.insert("id".to_string(), "submit-btn".to_string());
let xpath = serializer.generate_xpath_selector(&node);
assert_eq!(xpath, "//*[@id='submit-btn']");
node.attributes.clear();
node.attributes
.insert("name".to_string(), "username".to_string());
node.node_name = "input".to_string();
let xpath = serializer.generate_xpath_selector(&node);
assert_eq!(xpath, "//input[@name='username']");
node.attributes.clear();
node.attributes
.insert("data-testid".to_string(), "login-button".to_string());
node.node_name = "button".to_string();
let xpath = serializer.generate_xpath_selector(&node);
assert_eq!(xpath, "//*[@data-testid='login-button']");
node.attributes.clear();
node.attributes
.insert("id".to_string(), "test'quote".to_string());
let xpath = serializer.generate_xpath_selector(&node);
assert!(xpath.contains("\"test'quote\""));
}
#[test]
fn test_proxy_config_parsing() {
use browsing::config::parse_proxy_config_from_env;
unsafe { std::env::remove_var("HTTP_PROXY") };
unsafe { std::env::remove_var("HTTPS_PROXY") };
unsafe { std::env::remove_var("NO_PROXY") };
let proxy = parse_proxy_config_from_env();
assert!(proxy.is_none());
unsafe { std::env::set_var("HTTP_PROXY", "http://proxy.example.com:8080") };
let proxy = parse_proxy_config_from_env();
assert!(proxy.is_some());
assert_eq!(proxy.unwrap().server, "http://proxy.example.com:8080");
unsafe { std::env::set_var("HTTP_PROXY", "http://proxy.example.com:8080") };
unsafe { std::env::set_var("HTTPS_PROXY", "https://secure.proxy.com:3128") };
let proxy = parse_proxy_config_from_env();
assert!(proxy.is_some());
assert_eq!(proxy.unwrap().server, "http://proxy.example.com:8080");
unsafe { std::env::set_var("HTTP_PROXY", "http://proxy.example.com:8080") };
unsafe { std::env::set_var("NO_PROXY", "localhost,127.0.0.1,.example.com") };
let proxy = parse_proxy_config_from_env();
assert!(proxy.is_some());
assert_eq!(
proxy.unwrap().bypass,
Some("localhost,127.0.0.1,.example.com".to_string())
);
unsafe { std::env::set_var("HTTP_PROXY", "http://proxy.example.com:8080") };
unsafe { std::env::set_var("PROXY_USERNAME", "testuser") };
unsafe { std::env::set_var("PROXY_PASSWORD", "testpass") };
let proxy = parse_proxy_config_from_env();
assert!(proxy.is_some());
let config = proxy.unwrap();
assert_eq!(config.username, Some("testuser".to_string()));
assert_eq!(config.password, Some("testpass".to_string()));
unsafe { std::env::remove_var("HTTP_PROXY") };
unsafe { std::env::remove_var("HTTPS_PROXY") };
unsafe { std::env::remove_var("NO_PROXY") };
unsafe { std::env::remove_var("PROXY_USERNAME") };
unsafe { std::env::remove_var("PROXY_PASSWORD") };
}
#[test]
fn test_config_validation() {
use browsing::config::{AgentConfig, BrowserProfile, Config, LlmConfig};
let config = Config {
browser_profile: BrowserProfile {
headless: Some(true),
user_data_dir: None,
allowed_domains: None,
downloads_path: None,
proxy: None,
},
llm: LlmConfig {
api_key: Some("test-api-key-12345".to_string()),
model: Some("gpt-4".to_string()),
temperature: Some(0.7),
max_tokens: Some(1000),
},
agent: AgentConfig {
max_steps: Some(100),
use_vision: Some(false),
system_prompt: None,
},
};
assert!(config.validate().is_ok());
let invalid_config = Config {
llm: LlmConfig {
api_key: Some("".to_string()),
..Default::default()
},
..Default::default()
};
let errors = invalid_config.validate();
assert!(errors.is_err());
assert!(
errors
.unwrap_err()
.iter()
.any(|e| e.contains("API key is empty"))
);
let invalid_config = Config {
llm: LlmConfig {
api_key: Some("short".to_string()),
..Default::default()
},
..Default::default()
};
let errors = invalid_config.validate();
assert!(errors.is_err());
assert!(errors.unwrap_err().iter().any(|e| e.contains("too short")));
let invalid_config = Config {
llm: LlmConfig {
temperature: Some(3.0),
..Default::default()
},
..Default::default()
};
let errors = invalid_config.validate();
assert!(errors.is_err());
assert!(
errors
.unwrap_err()
.iter()
.any(|e| e.contains("out of range"))
);
let invalid_config = Config {
agent: AgentConfig {
max_steps: Some(0),
..Default::default()
},
..Default::default()
};
let errors = invalid_config.validate();
assert!(errors.is_err());
assert!(
errors
.unwrap_err()
.iter()
.any(|e| e.contains("cannot be zero"))
);
let invalid_config = Config {
browser_profile: BrowserProfile {
allowed_domains: Some(vec![]),
..Default::default()
},
..Default::default()
};
let errors = invalid_config.validate();
assert!(errors.is_err());
assert!(errors.unwrap_err().iter().any(|e| e.contains("empty")));
let invalid_config = Config {
browser_profile: BrowserProfile {
proxy: Some(browsing::browser::profile::ProxyConfig {
server: "invalid-url".to_string(),
bypass: None,
username: None,
password: None,
}),
..Default::default()
},
..Default::default()
};
let errors = invalid_config.validate();
assert!(errors.is_err());
assert!(
errors
.unwrap_err()
.iter()
.any(|e| e.contains("Invalid proxy URL"))
);
let invalid_config = Config {
llm: LlmConfig {
api_key: Some("".to_string()),
..Default::default()
},
..Default::default()
};
assert!(invalid_config.validate_or_error().is_err());
}