use std::io::Error as IoError;
use std::path::PathBuf;
use std::result::Result as StdResult;
use thiserror::Error;
use tokio::sync::oneshot::error::RecvError;
use tokio_tungstenite::tungstenite::Error as WsError;
use crate::identifiers::{ElementId, FrameId, RequestId, SessionId, TabId};
pub type Result<T> = StdResult<T, Error>;
#[derive(Error, Debug)]
pub enum Error {
#[error("Configuration error: {message}")]
Config {
message: String,
},
#[error("Profile error: {message}")]
Profile {
message: String,
},
#[error("Firefox not found at: {path}")]
FirefoxNotFound {
path: PathBuf,
},
#[error("Failed to launch Firefox: {message}")]
ProcessLaunchFailed {
message: String,
},
#[error("Connection failed: {message}")]
Connection {
message: String,
},
#[error("Connection timeout after {timeout_ms}ms")]
ConnectionTimeout {
timeout_ms: u64,
},
#[error("Connection closed")]
ConnectionClosed,
#[error("Unknown command: {command}")]
UnknownCommand {
command: String,
},
#[error("Invalid argument: {message}")]
InvalidArgument {
message: String,
},
#[error("Protocol error: {message}")]
Protocol {
message: String,
},
#[error("Element not found: selector={selector}, tab={tab_id}, frame={frame_id}")]
ElementNotFound {
selector: String,
tab_id: TabId,
frame_id: FrameId,
},
#[error("Stale element: {element_id}")]
StaleElement {
element_id: ElementId,
},
#[error("Frame not found: {frame_id}")]
FrameNotFound {
frame_id: FrameId,
},
#[error("Tab not found: {tab_id}")]
TabNotFound {
tab_id: TabId,
},
#[error("Script error: {message}")]
ScriptError {
message: String,
},
#[error("Timeout after {timeout_ms}ms: {operation}")]
Timeout {
operation: String,
timeout_ms: u64,
},
#[error("Request {request_id} timed out after {timeout_ms}ms")]
RequestTimeout {
request_id: RequestId,
timeout_ms: u64,
},
#[error("Intercept not found: {intercept_id}")]
InterceptNotFound {
intercept_id: String,
},
#[error("Session not found: {session_id}")]
SessionNotFound {
session_id: SessionId,
},
#[error("IO error: {0}")]
Io(#[from] IoError),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("WebSocket error: {0}")]
WebSocket(#[from] WsError),
#[error("Channel closed")]
ChannelClosed(#[from] RecvError),
}
impl Error {
#[inline]
pub fn config(message: impl Into<String>) -> Self {
Self::Config {
message: message.into(),
}
}
#[inline]
pub fn profile(message: impl Into<String>) -> Self {
Self::Profile {
message: message.into(),
}
}
#[inline]
pub fn firefox_not_found(path: impl Into<PathBuf>) -> Self {
Self::FirefoxNotFound { path: path.into() }
}
#[inline]
pub fn process_launch_failed(err: IoError) -> Self {
Self::ProcessLaunchFailed {
message: err.to_string(),
}
}
#[inline]
pub fn connection(message: impl Into<String>) -> Self {
Self::Connection {
message: message.into(),
}
}
#[inline]
pub fn connection_timeout(timeout_ms: u64) -> Self {
Self::ConnectionTimeout { timeout_ms }
}
#[inline]
pub fn protocol(message: impl Into<String>) -> Self {
Self::Protocol {
message: message.into(),
}
}
#[inline]
pub fn invalid_argument(message: impl Into<String>) -> Self {
Self::InvalidArgument {
message: message.into(),
}
}
#[inline]
pub fn element_not_found(
selector: impl Into<String>,
tab_id: TabId,
frame_id: FrameId,
) -> Self {
Self::ElementNotFound {
selector: selector.into(),
tab_id,
frame_id,
}
}
#[inline]
pub fn stale_element(element_id: ElementId) -> Self {
Self::StaleElement { element_id }
}
#[inline]
pub fn frame_not_found(frame_id: FrameId) -> Self {
Self::FrameNotFound { frame_id }
}
#[inline]
pub fn tab_not_found(tab_id: TabId) -> Self {
Self::TabNotFound { tab_id }
}
#[inline]
pub fn script_error(message: impl Into<String>) -> Self {
Self::ScriptError {
message: message.into(),
}
}
#[inline]
pub fn timeout(operation: impl Into<String>, timeout_ms: u64) -> Self {
Self::Timeout {
operation: operation.into(),
timeout_ms,
}
}
#[inline]
pub fn request_timeout(request_id: RequestId, timeout_ms: u64) -> Self {
Self::RequestTimeout {
request_id,
timeout_ms,
}
}
#[inline]
pub fn intercept_not_found(intercept_id: impl Into<String>) -> Self {
Self::InterceptNotFound {
intercept_id: intercept_id.into(),
}
}
#[inline]
pub fn session_not_found(session_id: SessionId) -> Self {
Self::SessionNotFound { session_id }
}
}
impl Error {
#[inline]
#[must_use]
pub fn is_timeout(&self) -> bool {
matches!(
self,
Self::ConnectionTimeout { .. } | Self::Timeout { .. } | Self::RequestTimeout { .. }
)
}
#[inline]
#[must_use]
pub fn is_element_error(&self) -> bool {
matches!(
self,
Self::ElementNotFound { .. } | Self::StaleElement { .. }
)
}
#[inline]
#[must_use]
pub fn is_connection_error(&self) -> bool {
matches!(
self,
Self::Connection { .. }
| Self::ConnectionTimeout { .. }
| Self::ConnectionClosed
| Self::WebSocket(_)
)
}
#[inline]
#[must_use]
pub fn is_recoverable(&self) -> bool {
matches!(
self,
Self::ConnectionTimeout { .. }
| Self::Timeout { .. }
| Self::RequestTimeout { .. }
| Self::StaleElement { .. }
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::ErrorKind;
#[test]
fn test_error_display() {
let err = Error::connection("failed to connect");
assert_eq!(err.to_string(), "Connection failed: failed to connect");
}
#[test]
fn test_config_error() {
let err = Error::config("missing binary path");
assert_eq!(err.to_string(), "Configuration error: missing binary path");
}
#[test]
fn test_is_timeout() {
let timeout_err = Error::ConnectionTimeout { timeout_ms: 5000 };
let other_err = Error::connection("test");
assert!(timeout_err.is_timeout());
assert!(!other_err.is_timeout());
}
#[test]
fn test_is_connection_error() {
let conn_err = Error::connection("test");
let timeout_err = Error::ConnectionTimeout { timeout_ms: 1000 };
let closed_err = Error::ConnectionClosed;
let other_err = Error::config("test");
assert!(conn_err.is_connection_error());
assert!(timeout_err.is_connection_error());
assert!(closed_err.is_connection_error());
assert!(!other_err.is_connection_error());
}
#[test]
fn test_is_recoverable() {
let timeout_err = Error::Timeout {
operation: "test".into(),
timeout_ms: 1000,
};
let config_err = Error::config("test");
assert!(timeout_err.is_recoverable());
assert!(!config_err.is_recoverable());
}
#[test]
fn test_from_io_error() {
let io_err = IoError::new(ErrorKind::NotFound, "file not found");
let err: Error = io_err.into();
assert!(matches!(err, Error::Io(_)));
}
#[test]
fn test_from_json_error() {
let json_err = serde_json::from_str::<String>("invalid").unwrap_err();
let err: Error = json_err.into();
assert!(matches!(err, Error::Json(_)));
}
}