1use thiserror::Error;
4
5pub type Result<T> = std::result::Result<T, Error>;
7
8#[derive(Debug, Error)]
12#[non_exhaustive]
13pub enum Error {
14 #[error("Failed to launch Chrome: {0}")]
16 Launch(String),
17
18 #[error(fmt = transport_fmt)]
20 Transport {
21 context: String,
23 #[source]
25 source: Option<std::io::Error>,
26 },
27
28 #[error(fmt = cdp_fmt)]
31 Cdp {
32 method: Option<String>,
34 code: Option<i64>,
36 message: String,
38 },
39
40 #[error("Navigation error: {0}")]
42 Navigation(String),
43
44 #[error("Element not found: {0}")]
46 ElementNotFound(String),
47
48 #[error("Element not visible: '{selector}' exists in DOM but is not rendered (hidden, display:none, or off-screen)")]
50 ElementNotVisible {
51 selector: String,
53 },
54
55 #[error("Timeout: {0}")]
57 Timeout(String),
58
59 #[error("Input state error: {0}")]
61 InputState(String),
62
63 #[error("Value mismatch for '{selector}': expected '{expected}', got '{actual}'")]
65 ValueMismatch {
66 selector: String,
68 expected: String,
70 actual: String,
72 },
73
74 #[error("Serialization error: {0}")]
76 Serialization(#[from] serde_json::Error),
77
78 #[error("Decode error: {0}")]
80 Decode(String),
81
82 #[error("IO error: {0}")]
84 Io(#[from] std::io::Error),
85
86 #[error("Chrome not found")]
88 ChromeNotFound,
89
90 #[error("Patching error in {operation}: {message}")]
92 Patching {
93 operation: String,
95 message: String,
97 },
98
99 #[error("Retry exhausted after {attempts} attempts: {last_error}")]
101 RetryExhausted {
102 attempts: u32,
104 last_error: String,
106 },
107}
108
109impl Clone for Error {
113 fn clone(&self) -> Self {
114 use serde::de::Error as _;
115 match self {
116 Self::Launch(s) => Self::Launch(s.clone()),
117 Self::Transport { context, source } => Self::Transport {
118 context: context.clone(),
119 source: source
120 .as_ref()
121 .map(|e| std::io::Error::new(e.kind(), e.to_string())),
122 },
123 Self::Cdp {
124 method,
125 code,
126 message,
127 } => Self::Cdp {
128 method: method.clone(),
129 code: *code,
130 message: message.clone(),
131 },
132 Self::Navigation(s) => Self::Navigation(s.clone()),
133 Self::ElementNotFound(s) => Self::ElementNotFound(s.clone()),
134 Self::ElementNotVisible { selector } => Self::ElementNotVisible {
135 selector: selector.clone(),
136 },
137 Self::Timeout(s) => Self::Timeout(s.clone()),
138 Self::InputState(s) => Self::InputState(s.clone()),
139 Self::ValueMismatch {
140 selector,
141 expected,
142 actual,
143 } => Self::ValueMismatch {
144 selector: selector.clone(),
145 expected: expected.clone(),
146 actual: actual.clone(),
147 },
148 Self::Serialization(e) => Self::Serialization(serde_json::Error::custom(e.to_string())),
149 Self::Decode(s) => Self::Decode(s.clone()),
150 Self::Io(e) => Self::Io(std::io::Error::new(e.kind(), e.to_string())),
151 Self::ChromeNotFound => Self::ChromeNotFound,
152 Self::Patching { operation, message } => Self::Patching {
153 operation: operation.clone(),
154 message: message.clone(),
155 },
156 Self::RetryExhausted {
157 attempts,
158 last_error,
159 } => Self::RetryExhausted {
160 attempts: *attempts,
161 last_error: last_error.clone(),
162 },
163 }
164 }
165}
166
167fn cdp_fmt(
169 method: &Option<String>,
170 code: &Option<i64>,
171 message: &str,
172 f: &mut std::fmt::Formatter<'_>,
173) -> std::fmt::Result {
174 match (method, code) {
175 (Some(m), Some(c)) => write!(f, "CDP error in {}: {} (code {})", m, message, c),
176 (Some(m), None) => write!(f, "CDP error in {}: {}", m, message),
177 _ => write!(f, "CDP error: {}", message),
178 }
179}
180
181fn transport_fmt(
183 context: &str,
184 source: &Option<std::io::Error>,
185 f: &mut std::fmt::Formatter<'_>,
186) -> std::fmt::Result {
187 match source {
188 Some(err) => write!(f, "Transport error: {}: {}", context, err),
189 None => write!(f, "Transport error: {}", context),
190 }
191}
192
193impl Error {
194 pub fn transport(context: impl Into<String>) -> Self {
196 Self::Transport {
197 context: context.into(),
198 source: None,
199 }
200 }
201
202 pub fn transport_io(context: impl Into<String>, source: std::io::Error) -> Self {
204 Self::Transport {
205 context: context.into(),
206 source: Some(source),
207 }
208 }
209
210 pub fn cdp(method: impl Into<String>, code: i64, message: impl Into<String>) -> Self {
212 Self::Cdp {
213 method: Some(method.into()),
214 code: Some(code),
215 message: message.into(),
216 }
217 }
218
219 pub fn cdp_msg(message: impl Into<String>) -> Self {
221 Self::Cdp {
222 method: None,
223 code: None,
224 message: message.into(),
225 }
226 }
227
228 pub fn patching(operation: impl Into<String>, message: impl Into<String>) -> Self {
230 Self::Patching {
231 operation: operation.into(),
232 message: message.into(),
233 }
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use std::error::Error as StdError;
241
242 #[test]
243 fn test_transport_io_display_includes_context_and_cause() {
244 let err = Error::transport_io(
245 "Failed to connect",
246 std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "boom"),
247 );
248 let msg = format!("{}", err);
249 assert!(
251 msg.contains("Failed to connect"),
252 "missing context in: {msg}"
253 );
254 assert!(msg.contains("boom"), "missing io cause in: {msg}");
255 }
256
257 #[test]
258 fn test_transport_io_exposes_source() {
259 let err = Error::transport_io(
260 "Failed to connect",
261 std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "boom"),
262 );
263 assert!(
264 err.source().is_some(),
265 "transport_io error should expose its io source"
266 );
267 }
268
269 #[test]
270 fn test_cdp_display_with_and_without_context() {
271 let full = Error::cdp("DOM.resolveNode", -1, "boom");
272 assert_eq!(
273 format!("{full}"),
274 "CDP error in DOM.resolveNode: boom (code -1)"
275 );
276 let simple = Error::cdp_msg("no value");
277 assert_eq!(format!("{simple}"), "CDP error: no value");
278 }
279
280 #[test]
281 fn test_transport_display_plain_message() {
282 let err = Error::transport("plain msg");
283 let msg = format!("{}", err);
284 assert!(msg.contains("plain msg"), "missing message in: {msg}");
285 assert!(err.source().is_none());
287 }
288}