use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PageTargetLostDetails {
pub operation: String,
pub detail: String,
pub recoverable: bool,
pub recovery_hint: Option<String>,
}
impl PageTargetLostDetails {
pub fn recoverable(operation: impl Into<String>, detail: impl Into<String>) -> Self {
Self {
operation: operation.into(),
detail: detail.into(),
recoverable: true,
recovery_hint: None,
}
}
pub fn attach_degraded(
operation: impl Into<String>,
detail: impl Into<String>,
recovery_hint: impl Into<String>,
) -> Self {
Self {
operation: operation.into(),
detail: detail.into(),
recoverable: false,
recovery_hint: Some(recovery_hint.into()),
}
}
pub fn is_attach_session_degraded(&self) -> bool {
!self.recoverable && self.recovery_hint.is_some()
}
}
impl std::fmt::Display for PageTargetLostDetails {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"operation={} recoverable={} detail={}",
self.operation, self.recoverable, self.detail
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackendUnsupportedDetails {
pub capability: String,
pub operation: String,
}
impl BackendUnsupportedDetails {
pub fn new(capability: impl Into<String>, operation: impl Into<String>) -> Self {
Self {
capability: capability.into(),
operation: operation.into(),
}
}
}
impl std::fmt::Display for BackendUnsupportedDetails {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"Browser backend does not support {} during {}",
self.capability, self.operation
)
}
}
#[derive(Error, Debug)]
pub enum BrowserError {
#[error("Failed to launch browser: {0}")]
LaunchFailed(String),
#[error("Failed to connect to browser: {0}")]
ConnectionFailed(String),
#[error("Operation timed out: {0}")]
Timeout(String),
#[error("Invalid selector: {0}")]
SelectorInvalid(String),
#[error("Element not found: {0}")]
ElementNotFound(String),
#[error("Failed to parse DOM: {0}")]
DomParseFailed(String),
#[error("Tool '{tool}' execution failed: {reason}")]
ToolExecutionFailed { tool: String, reason: String },
#[error("Invalid argument: {0}")]
InvalidArgument(String),
#[error("Navigation failed: {0}")]
NavigationFailed(String),
#[error("JavaScript evaluation failed: {0}")]
EvaluationFailed(String),
#[error("Screenshot failed: {0}")]
ScreenshotFailed(String),
#[error("Download failed: {0}")]
DownloadFailed(String),
#[error("Tab operation failed: {0}")]
TabOperationFailed(String),
#[error("Page target lost: {0}")]
PageTargetLost(PageTargetLostDetails),
#[error("Backend unsupported: {0}")]
BackendUnsupported(BackendUnsupportedDetails),
#[error("Chrome error: {0}")]
ChromeError(String),
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
}
pub type Result<T> = std::result::Result<T, BrowserError>;
impl From<anyhow::Error> for BrowserError {
fn from(err: anyhow::Error) -> Self {
BrowserError::ChromeError(err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = BrowserError::LaunchFailed("Chrome not found".to_string());
assert_eq!(
err.to_string(),
"Failed to launch browser: Chrome not found"
);
}
#[test]
fn test_tool_execution_error() {
let err = BrowserError::ToolExecutionFailed {
tool: "navigate".to_string(),
reason: "Invalid URL".to_string(),
};
assert_eq!(
err.to_string(),
"Tool 'navigate' execution failed: Invalid URL"
);
}
#[test]
fn test_json_error_conversion() {
let json_err = serde_json::from_str::<serde_json::Value>("invalid json");
assert!(json_err.is_err());
let browser_err: BrowserError = json_err.unwrap_err().into();
assert!(matches!(browser_err, BrowserError::JsonError(_)));
}
#[test]
fn test_result_type_alias() {
fn example_function() -> Result<String> {
Err(BrowserError::InvalidArgument("test".to_string()))
}
let result = example_function();
assert!(result.is_err());
}
}