use thiserror::Error;
pub type Result<T> = std::result::Result<T, BrowserError>;
#[derive(Error, Debug)]
pub enum BrowserError {
#[error("Browser launch failed: {reason}")]
LaunchFailed {
reason: String,
},
#[error("CDP error during '{operation}': {message}")]
CdpError {
operation: String,
message: String,
},
#[error("Browser pool exhausted (active={active}, max={max})")]
PoolExhausted {
active: usize,
max: usize,
},
#[error("Timeout after {duration_ms}ms during '{operation}'")]
Timeout {
operation: String,
duration_ms: u64,
},
#[error("Navigation to '{url}' failed: {reason}")]
NavigationFailed {
url: String,
reason: String,
},
#[error("Script execution failed: {reason}")]
ScriptExecutionFailed {
script: String,
reason: String,
},
#[error("Browser connection error: {reason}")]
ConnectionError {
url: String,
reason: String,
},
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Stale node handle (selector: {selector})")]
StaleNode {
selector: String,
},
#[cfg(feature = "extract")]
#[error("extraction failed: {0}")]
ExtractionFailed(#[from] crate::extract::ExtractionError),
}
impl From<chromiumoxide::error::CdpError> for BrowserError {
fn from(err: chromiumoxide::error::CdpError) -> Self {
Self::CdpError {
operation: "unknown".to_string(),
message: err.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn launch_failed_display() {
let e = BrowserError::LaunchFailed {
reason: "binary not found".to_string(),
};
assert!(e.to_string().contains("binary not found"));
}
#[test]
fn pool_exhausted_display() {
let e = BrowserError::PoolExhausted {
active: 10,
max: 10,
};
assert!(e.to_string().contains("10"));
}
#[test]
fn navigation_failed_includes_url() {
let e = BrowserError::NavigationFailed {
url: "https://example.com".to_string(),
reason: "DNS failure".to_string(),
};
assert!(e.to_string().contains("example.com"));
assert!(e.to_string().contains("DNS failure"));
}
#[test]
fn timeout_display() {
let e = BrowserError::Timeout {
operation: "page.load".to_string(),
duration_ms: 30_000,
};
assert!(e.to_string().contains("30000"));
}
#[test]
fn cdp_error_display() {
let e = BrowserError::CdpError {
operation: "Page.navigate".to_string(),
message: "Target closed".to_string(),
};
let s = e.to_string();
assert!(s.contains("Page.navigate"));
assert!(s.contains("Target closed"));
}
#[test]
fn script_execution_failed_display() {
let e = BrowserError::ScriptExecutionFailed {
script: "document.title".to_string(),
reason: "Execution context destroyed".to_string(),
};
assert!(e.to_string().contains("Execution context destroyed"));
}
#[test]
fn connection_error_display() {
let e = BrowserError::ConnectionError {
url: "ws://127.0.0.1:9222/json/version".to_string(),
reason: "connection refused".to_string(),
};
let s = e.to_string();
assert!(s.contains("connection refused"));
}
#[test]
fn config_error_display() {
let e = BrowserError::ConfigError("pool.max_size must be >= 1".to_string());
assert!(e.to_string().contains("pool.max_size"));
}
#[test]
fn io_error_wraps_std() {
let io = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let e = BrowserError::Io(io);
assert!(e.to_string().contains("file not found"));
}
#[test]
fn launch_failed_is_debug_printable() {
let e = BrowserError::LaunchFailed {
reason: "test".to_string(),
};
assert!(!format!("{e:?}").is_empty());
}
#[test]
fn pool_exhausted_reports_both_counts() {
let e = BrowserError::PoolExhausted { active: 5, max: 5 };
let s = e.to_string();
assert!(s.contains("active=5"));
assert!(s.contains("max=5"));
}
#[test]
fn stale_node_display_contains_selector() {
let e = BrowserError::StaleNode {
selector: "[data-ux=\"Section\"]".to_string(),
};
let s = e.to_string();
assert!(s.contains("[data-ux=\"Section\"]"), "display: {s}");
}
#[test]
fn stale_node_is_debug_printable() {
let e = BrowserError::StaleNode {
selector: "div.foo".to_string(),
};
assert!(!format!("{e:?}").is_empty());
}
#[test]
fn node_handle_stale_error_display() {
let e = BrowserError::StaleNode {
selector: "div.foo".to_string(),
};
let s = e.to_string().to_lowercase();
assert!(
s.contains("div.foo"),
"display should contain selector: {s}"
);
assert!(s.contains("stale"), "display should contain 'stale': {s}");
}
}