use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
#[error("Failed to launch Chrome: {0}")]
Launch(String),
#[error(fmt = transport_fmt)]
Transport {
context: String,
#[source]
source: Option<std::io::Error>,
},
#[error(fmt = cdp_fmt)]
Cdp {
method: Option<String>,
code: Option<i64>,
message: String,
},
#[error("Navigation error: {0}")]
Navigation(String),
#[error("Element not found: {0}")]
ElementNotFound(String),
#[error("Element not visible: '{selector}' exists in DOM but is not rendered (hidden, display:none, or off-screen)")]
ElementNotVisible {
selector: String,
},
#[error("Timeout: {0}")]
Timeout(String),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Decode error: {0}")]
Decode(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Chrome not found")]
ChromeNotFound,
#[error("Patching error in {operation}: {message}")]
Patching {
operation: String,
message: String,
},
#[error("Retry exhausted after {attempts} attempts: {last_error}")]
RetryExhausted {
attempts: u32,
last_error: String,
},
}
impl Clone for Error {
fn clone(&self) -> Self {
use serde::de::Error as _;
match self {
Self::Launch(s) => Self::Launch(s.clone()),
Self::Transport { context, source } => Self::Transport {
context: context.clone(),
source: source
.as_ref()
.map(|e| std::io::Error::new(e.kind(), e.to_string())),
},
Self::Cdp {
method,
code,
message,
} => Self::Cdp {
method: method.clone(),
code: *code,
message: message.clone(),
},
Self::Navigation(s) => Self::Navigation(s.clone()),
Self::ElementNotFound(s) => Self::ElementNotFound(s.clone()),
Self::ElementNotVisible { selector } => Self::ElementNotVisible {
selector: selector.clone(),
},
Self::Timeout(s) => Self::Timeout(s.clone()),
Self::Serialization(e) => Self::Serialization(serde_json::Error::custom(e.to_string())),
Self::Decode(s) => Self::Decode(s.clone()),
Self::Io(e) => Self::Io(std::io::Error::new(e.kind(), e.to_string())),
Self::ChromeNotFound => Self::ChromeNotFound,
Self::Patching { operation, message } => Self::Patching {
operation: operation.clone(),
message: message.clone(),
},
Self::RetryExhausted {
attempts,
last_error,
} => Self::RetryExhausted {
attempts: *attempts,
last_error: last_error.clone(),
},
}
}
}
fn cdp_fmt(
method: &Option<String>,
code: &Option<i64>,
message: &str,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
match (method, code) {
(Some(m), Some(c)) => write!(f, "CDP error in {}: {} (code {})", m, message, c),
(Some(m), None) => write!(f, "CDP error in {}: {}", m, message),
_ => write!(f, "CDP error: {}", message),
}
}
fn transport_fmt(
context: &str,
source: &Option<std::io::Error>,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
match source {
Some(err) => write!(f, "Transport error: {}: {}", context, err),
None => write!(f, "Transport error: {}", context),
}
}
impl Error {
pub fn transport(context: impl Into<String>) -> Self {
Self::Transport {
context: context.into(),
source: None,
}
}
pub fn transport_io(context: impl Into<String>, source: std::io::Error) -> Self {
Self::Transport {
context: context.into(),
source: Some(source),
}
}
pub fn cdp(method: impl Into<String>, code: i64, message: impl Into<String>) -> Self {
Self::Cdp {
method: Some(method.into()),
code: Some(code),
message: message.into(),
}
}
pub fn cdp_msg(message: impl Into<String>) -> Self {
Self::Cdp {
method: None,
code: None,
message: message.into(),
}
}
pub fn patching(operation: impl Into<String>, message: impl Into<String>) -> Self {
Self::Patching {
operation: operation.into(),
message: message.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error as StdError;
#[test]
fn test_transport_io_display_includes_context_and_cause() {
let err = Error::transport_io(
"Failed to connect",
std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "boom"),
);
let msg = format!("{}", err);
assert!(
msg.contains("Failed to connect"),
"missing context in: {msg}"
);
assert!(msg.contains("boom"), "missing io cause in: {msg}");
}
#[test]
fn test_transport_io_exposes_source() {
let err = Error::transport_io(
"Failed to connect",
std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "boom"),
);
assert!(
err.source().is_some(),
"transport_io error should expose its io source"
);
}
#[test]
fn test_cdp_display_with_and_without_context() {
let full = Error::cdp("DOM.resolveNode", -1, "boom");
assert_eq!(
format!("{full}"),
"CDP error in DOM.resolveNode: boom (code -1)"
);
let simple = Error::cdp_msg("no value");
assert_eq!(format!("{simple}"), "CDP error: no value");
}
#[test]
fn test_transport_display_plain_message() {
let err = Error::transport("plain msg");
let msg = format!("{}", err);
assert!(msg.contains("plain msg"), "missing message in: {msg}");
assert!(err.source().is_none());
}
}