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("Action {code}: {source}")]
16 Action {
17 code: String,
19 receipt: Box<crate::ActionReceipt>,
21 #[source]
22 source: Box<Error>,
24 },
25 #[error("Failed to launch Chrome: {0}")]
27 Launch(String),
28
29 #[error(fmt = transport_fmt)]
31 Transport {
32 context: String,
34 #[source]
36 source: Option<std::io::Error>,
37 },
38
39 #[error(fmt = cdp_fmt)]
42 Cdp {
43 method: Option<String>,
45 code: Option<i64>,
47 message: String,
49 },
50
51 #[error("Navigation error: {0}")]
53 Navigation(String),
54
55 #[error("Element not found: {0}")]
57 ElementNotFound(String),
58
59 #[error("Element not visible: '{selector}' exists in DOM but is not rendered (hidden, display:none, or off-screen)")]
61 ElementNotVisible {
62 selector: String,
64 },
65
66 #[error("Timeout: {0}")]
68 Timeout(String),
69
70 #[error("Input state error: {0}")]
72 InputState(String),
73
74 #[error("Value mismatch for '{selector}': expected '{expected}', got '{actual}'")]
76 ValueMismatch {
77 selector: String,
79 expected: String,
81 actual: String,
83 },
84
85 #[error("Serialization error: {0}")]
87 Serialization(#[from] serde_json::Error),
88
89 #[error("Decode error: {0}")]
91 Decode(String),
92
93 #[error("IO error: {0}")]
95 Io(#[from] std::io::Error),
96
97 #[error("Chrome not found")]
99 ChromeNotFound,
100
101 #[error("Patching error in {operation}: {message}")]
103 Patching {
104 operation: String,
106 message: String,
108 },
109
110 #[error("Retry exhausted after {attempts} attempts: {last_error}")]
112 RetryExhausted {
113 attempts: u32,
115 last_error: String,
117 },
118}
119
120impl Clone for Error {
124 fn clone(&self) -> Self {
125 use serde::de::Error as _;
126 match self {
127 Self::Action {
128 code,
129 receipt,
130 source,
131 } => Self::Action {
132 code: code.clone(),
133 receipt: receipt.clone(),
134 source: source.clone(),
135 },
136 Self::Launch(s) => Self::Launch(s.clone()),
137 Self::Transport { context, source } => Self::Transport {
138 context: context.clone(),
139 source: source
140 .as_ref()
141 .map(|e| std::io::Error::new(e.kind(), e.to_string())),
142 },
143 Self::Cdp {
144 method,
145 code,
146 message,
147 } => Self::Cdp {
148 method: method.clone(),
149 code: *code,
150 message: message.clone(),
151 },
152 Self::Navigation(s) => Self::Navigation(s.clone()),
153 Self::ElementNotFound(s) => Self::ElementNotFound(s.clone()),
154 Self::ElementNotVisible { selector } => Self::ElementNotVisible {
155 selector: selector.clone(),
156 },
157 Self::Timeout(s) => Self::Timeout(s.clone()),
158 Self::InputState(s) => Self::InputState(s.clone()),
159 Self::ValueMismatch {
160 selector,
161 expected,
162 actual,
163 } => Self::ValueMismatch {
164 selector: selector.clone(),
165 expected: expected.clone(),
166 actual: actual.clone(),
167 },
168 Self::Serialization(e) => Self::Serialization(serde_json::Error::custom(e.to_string())),
169 Self::Decode(s) => Self::Decode(s.clone()),
170 Self::Io(e) => Self::Io(std::io::Error::new(e.kind(), e.to_string())),
171 Self::ChromeNotFound => Self::ChromeNotFound,
172 Self::Patching { operation, message } => Self::Patching {
173 operation: operation.clone(),
174 message: message.clone(),
175 },
176 Self::RetryExhausted {
177 attempts,
178 last_error,
179 } => Self::RetryExhausted {
180 attempts: *attempts,
181 last_error: last_error.clone(),
182 },
183 }
184 }
185}
186
187fn cdp_fmt(
189 method: &Option<String>,
190 code: &Option<i64>,
191 message: &str,
192 f: &mut std::fmt::Formatter<'_>,
193) -> std::fmt::Result {
194 match (method, code) {
195 (Some(m), Some(c)) => write!(f, "CDP error in {}: {} (code {})", m, message, c),
196 (Some(m), None) => write!(f, "CDP error in {}: {}", m, message),
197 _ => write!(f, "CDP error: {}", message),
198 }
199}
200
201fn transport_fmt(
203 context: &str,
204 source: &Option<std::io::Error>,
205 f: &mut std::fmt::Formatter<'_>,
206) -> std::fmt::Result {
207 match source {
208 Some(err) => write!(f, "Transport error: {}: {}", context, err),
209 None => write!(f, "Transport error: {}", context),
210 }
211}
212
213impl Error {
214 pub fn transport(context: impl Into<String>) -> Self {
216 Self::Transport {
217 context: context.into(),
218 source: None,
219 }
220 }
221
222 pub fn transport_io(context: impl Into<String>, source: std::io::Error) -> Self {
224 Self::Transport {
225 context: context.into(),
226 source: Some(source),
227 }
228 }
229
230 pub fn cdp(method: impl Into<String>, code: i64, message: impl Into<String>) -> Self {
232 Self::Cdp {
233 method: Some(method.into()),
234 code: Some(code),
235 message: message.into(),
236 }
237 }
238
239 pub fn cdp_msg(message: impl Into<String>) -> Self {
241 Self::Cdp {
242 method: None,
243 code: None,
244 message: message.into(),
245 }
246 }
247
248 pub fn patching(operation: impl Into<String>, message: impl Into<String>) -> Self {
250 Self::Patching {
251 operation: operation.into(),
252 message: message.into(),
253 }
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use std::error::Error as StdError;
261
262 #[test]
263 fn test_transport_io_display_includes_context_and_cause() {
264 let err = Error::transport_io(
265 "Failed to connect",
266 std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "boom"),
267 );
268 let msg = format!("{}", err);
269 assert!(
271 msg.contains("Failed to connect"),
272 "missing context in: {msg}"
273 );
274 assert!(msg.contains("boom"), "missing io cause in: {msg}");
275 }
276
277 #[test]
278 fn test_transport_io_exposes_source() {
279 let err = Error::transport_io(
280 "Failed to connect",
281 std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "boom"),
282 );
283 assert!(
284 err.source().is_some(),
285 "transport_io error should expose its io source"
286 );
287 }
288
289 #[test]
290 fn test_cdp_display_with_and_without_context() {
291 let full = Error::cdp("DOM.resolveNode", -1, "boom");
292 assert_eq!(
293 format!("{full}"),
294 "CDP error in DOM.resolveNode: boom (code -1)"
295 );
296 let simple = Error::cdp_msg("no value");
297 assert_eq!(format!("{simple}"), "CDP error: no value");
298 }
299
300 #[test]
301 fn test_transport_display_plain_message() {
302 let err = Error::transport("plain msg");
303 let msg = format!("{}", err);
304 assert!(msg.contains("plain msg"), "missing message in: {msg}");
305 assert!(err.source().is_none());
307 }
308}