firefox_webdriver/error.rs
1//! Error types for Firefox WebDriver.
2//!
3//! This module defines all error types used throughout the crate.
4//! Error codes follow ARCHITECTURE.md Section 6.2.
5//!
6//! # Usage
7//!
8//! All fallible operations return [`Result<T>`] which uses [`Error`]:
9//!
10//! ```ignore
11//! use firefox_webdriver::{Result, Error};
12//!
13//! async fn example(tab: &Tab) -> Result<()> {
14//! let element = tab.find_element("#submit").await?;
15//! element.click().await?;
16//! Ok(())
17//! }
18//! ```
19//!
20//! # Error Categories
21//!
22//! | Category | Variants |
23//! |----------|----------|
24//! | Configuration | [`Error::Config`], [`Error::Profile`] |
25//! | Connection | [`Error::Connection`], [`Error::ConnectionTimeout`], [`Error::ConnectionClosed`] |
26//! | Protocol | [`Error::UnknownCommand`], [`Error::InvalidArgument`], [`Error::Protocol`] |
27//! | Element | [`Error::ElementNotFound`], [`Error::StaleElement`] |
28//! | Navigation | [`Error::FrameNotFound`], [`Error::TabNotFound`] |
29//! | Execution | [`Error::ScriptError`], [`Error::Timeout`], [`Error::RequestTimeout`] |
30//! | External | [`Error::Io`], [`Error::Json`], [`Error::WebSocket`] |
31
32// ============================================================================
33// Imports
34// ============================================================================
35
36use std::io::Error as IoError;
37use std::path::PathBuf;
38use std::result::Result as StdResult;
39
40use thiserror::Error;
41use tokio::sync::oneshot::error::RecvError;
42use tokio_tungstenite::tungstenite::Error as WsError;
43
44use crate::identifiers::{ElementId, FrameId, RequestId, SessionId, TabId};
45
46// ============================================================================
47// Result Alias
48// ============================================================================
49
50/// Result type alias using crate [`enum@Error`].
51///
52/// All fallible operations in this crate return this type.
53pub type Result<T> = StdResult<T, Error>;
54
55// ============================================================================
56// Error Enum
57// ============================================================================
58
59/// Main error type for the crate.
60///
61/// Each variant includes relevant context for debugging.
62/// Error codes match ARCHITECTURE.md Section 6.2.
63#[derive(Error, Debug)]
64pub enum Error {
65 // ========================================================================
66 // Configuration Errors
67 // ========================================================================
68 /// Configuration error.
69 ///
70 /// Returned when driver configuration is invalid.
71 #[error("Configuration error: {message}")]
72 Config {
73 /// Description of the configuration error.
74 message: String,
75 },
76
77 /// Profile error.
78 ///
79 /// Returned when Firefox profile creation or setup fails.
80 #[error("Profile error: {message}")]
81 Profile {
82 /// Description of the profile error.
83 message: String,
84 },
85
86 /// Firefox binary not found at path.
87 ///
88 /// Returned when the specified Firefox binary does not exist.
89 #[error("Firefox not found at: {path}")]
90 FirefoxNotFound {
91 /// Path where Firefox was expected.
92 path: PathBuf,
93 },
94
95 /// Failed to launch Firefox process.
96 ///
97 /// Returned when Firefox process fails to start.
98 #[error("Failed to launch Firefox: {message}")]
99 ProcessLaunchFailed {
100 /// Description of the launch failure.
101 message: String,
102 },
103
104 // ========================================================================
105 // Connection Errors
106 // ========================================================================
107 /// WebSocket connection failed.
108 ///
109 /// Returned when WebSocket connection cannot be established.
110 #[error("Connection failed: {message}")]
111 Connection {
112 /// Description of the connection error.
113 message: String,
114 },
115
116 /// Connection timeout waiting for extension.
117 ///
118 /// Returned when extension does not connect within timeout period.
119 #[error("Connection timeout after {timeout_ms}ms")]
120 ConnectionTimeout {
121 /// Milliseconds waited before timeout.
122 timeout_ms: u64,
123 },
124
125 /// WebSocket connection closed unexpectedly.
126 ///
127 /// Returned when connection is lost during operation.
128 #[error("Connection closed")]
129 ConnectionClosed,
130
131 // ========================================================================
132 // Protocol Errors
133 // ========================================================================
134 /// Unknown command method.
135 ///
136 /// Returned when extension receives unrecognized command.
137 #[error("Unknown command: {command}")]
138 UnknownCommand {
139 /// The unrecognized command method.
140 command: String,
141 },
142
143 /// Invalid argument in command params.
144 ///
145 /// Returned when command parameters are invalid.
146 #[error("Invalid argument: {message}")]
147 InvalidArgument {
148 /// Description of the invalid argument.
149 message: String,
150 },
151
152 /// Protocol violation or unexpected response.
153 ///
154 /// Returned when protocol message format is invalid.
155 #[error("Protocol error: {message}")]
156 Protocol {
157 /// Description of the protocol violation.
158 message: String,
159 },
160
161 // ========================================================================
162 // Element Errors
163 // ========================================================================
164 /// Element not found by selector.
165 ///
166 /// Returned when CSS selector matches no elements.
167 #[error("Element not found: selector={selector}, tab={tab_id}, frame={frame_id}")]
168 ElementNotFound {
169 /// CSS selector used.
170 selector: String,
171 /// Tab where search was performed.
172 tab_id: TabId,
173 /// Frame where search was performed.
174 frame_id: FrameId,
175 },
176
177 /// Element is stale (no longer in DOM).
178 ///
179 /// Returned when element reference is no longer valid.
180 #[error("Stale element: {element_id}")]
181 StaleElement {
182 /// The stale element's ID.
183 element_id: ElementId,
184 },
185
186 // ========================================================================
187 // Navigation Errors
188 // ========================================================================
189 /// Frame not found.
190 ///
191 /// Returned when frame ID does not exist.
192 #[error("Frame not found: {frame_id}")]
193 FrameNotFound {
194 /// The missing frame ID.
195 frame_id: FrameId,
196 },
197
198 /// Tab not found.
199 ///
200 /// Returned when tab ID does not exist.
201 #[error("Tab not found: {tab_id}")]
202 TabNotFound {
203 /// The missing tab ID.
204 tab_id: TabId,
205 },
206
207 // ========================================================================
208 // Execution Errors
209 // ========================================================================
210 /// JavaScript execution error.
211 ///
212 /// Returned when script execution fails in browser.
213 #[error("Script error: {message}")]
214 ScriptError {
215 /// Error message from script execution.
216 message: String,
217 },
218
219 /// Operation timeout.
220 ///
221 /// Returned when operation exceeds timeout duration.
222 #[error("Timeout after {timeout_ms}ms: {operation}")]
223 Timeout {
224 /// Description of the operation that timed out.
225 operation: String,
226 /// Milliseconds waited before timeout.
227 timeout_ms: u64,
228 },
229
230 /// Command request timeout.
231 ///
232 /// Returned when WebSocket request times out.
233 #[error("Request {request_id} timed out after {timeout_ms}ms")]
234 RequestTimeout {
235 /// The request ID that timed out.
236 request_id: RequestId,
237 /// Milliseconds waited before timeout.
238 timeout_ms: u64,
239 },
240
241 // ========================================================================
242 // Network Errors
243 // ========================================================================
244 /// Network intercept not found.
245 ///
246 /// Returned when intercept ID does not exist.
247 #[error("Intercept not found: {intercept_id}")]
248 InterceptNotFound {
249 /// The missing intercept ID.
250 intercept_id: String,
251 },
252
253 /// Session not found in connection pool.
254 ///
255 /// Returned when session ID does not exist in the pool.
256 #[error("Session not found: {session_id}")]
257 SessionNotFound {
258 /// The missing session ID.
259 session_id: SessionId,
260 },
261
262 // ========================================================================
263 // External Errors
264 // ========================================================================
265 /// IO error.
266 #[error("IO error: {0}")]
267 Io(#[from] IoError),
268
269 /// JSON serialization error.
270 #[error("JSON error: {0}")]
271 Json(#[from] serde_json::Error),
272
273 /// WebSocket error.
274 #[error("WebSocket error: {0}")]
275 WebSocket(#[from] WsError),
276
277 /// Channel receive error.
278 #[error("Channel closed")]
279 ChannelClosed(#[from] RecvError),
280}
281
282// ============================================================================
283// Error Constructors
284// ============================================================================
285
286impl Error {
287 /// Creates a configuration error.
288 #[inline]
289 pub fn config(message: impl Into<String>) -> Self {
290 Self::Config {
291 message: message.into(),
292 }
293 }
294
295 /// Creates a profile error.
296 #[inline]
297 pub fn profile(message: impl Into<String>) -> Self {
298 Self::Profile {
299 message: message.into(),
300 }
301 }
302
303 /// Creates a Firefox not found error.
304 #[inline]
305 pub fn firefox_not_found(path: impl Into<PathBuf>) -> Self {
306 Self::FirefoxNotFound { path: path.into() }
307 }
308
309 /// Creates a process launch failed error.
310 #[inline]
311 pub fn process_launch_failed(err: IoError) -> Self {
312 Self::ProcessLaunchFailed {
313 message: err.to_string(),
314 }
315 }
316
317 /// Creates a connection error.
318 #[inline]
319 pub fn connection(message: impl Into<String>) -> Self {
320 Self::Connection {
321 message: message.into(),
322 }
323 }
324
325 /// Creates a connection timeout error.
326 #[inline]
327 pub fn connection_timeout(timeout_ms: u64) -> Self {
328 Self::ConnectionTimeout { timeout_ms }
329 }
330
331 /// Creates a protocol error.
332 #[inline]
333 pub fn protocol(message: impl Into<String>) -> Self {
334 Self::Protocol {
335 message: message.into(),
336 }
337 }
338
339 /// Creates an invalid argument error.
340 #[inline]
341 pub fn invalid_argument(message: impl Into<String>) -> Self {
342 Self::InvalidArgument {
343 message: message.into(),
344 }
345 }
346
347 /// Creates an element not found error.
348 #[inline]
349 pub fn element_not_found(
350 selector: impl Into<String>,
351 tab_id: TabId,
352 frame_id: FrameId,
353 ) -> Self {
354 Self::ElementNotFound {
355 selector: selector.into(),
356 tab_id,
357 frame_id,
358 }
359 }
360
361 /// Creates a stale element error.
362 #[inline]
363 pub fn stale_element(element_id: ElementId) -> Self {
364 Self::StaleElement { element_id }
365 }
366
367 /// Creates a frame not found error.
368 #[inline]
369 pub fn frame_not_found(frame_id: FrameId) -> Self {
370 Self::FrameNotFound { frame_id }
371 }
372
373 /// Creates a tab not found error.
374 #[inline]
375 pub fn tab_not_found(tab_id: TabId) -> Self {
376 Self::TabNotFound { tab_id }
377 }
378
379 /// Creates a script error.
380 #[inline]
381 pub fn script_error(message: impl Into<String>) -> Self {
382 Self::ScriptError {
383 message: message.into(),
384 }
385 }
386
387 /// Creates a timeout error.
388 #[inline]
389 pub fn timeout(operation: impl Into<String>, timeout_ms: u64) -> Self {
390 Self::Timeout {
391 operation: operation.into(),
392 timeout_ms,
393 }
394 }
395
396 /// Creates a request timeout error.
397 #[inline]
398 pub fn request_timeout(request_id: RequestId, timeout_ms: u64) -> Self {
399 Self::RequestTimeout {
400 request_id,
401 timeout_ms,
402 }
403 }
404
405 /// Creates an intercept not found error.
406 #[inline]
407 pub fn intercept_not_found(intercept_id: impl Into<String>) -> Self {
408 Self::InterceptNotFound {
409 intercept_id: intercept_id.into(),
410 }
411 }
412
413 /// Creates a session not found error.
414 #[inline]
415 pub fn session_not_found(session_id: SessionId) -> Self {
416 Self::SessionNotFound { session_id }
417 }
418}
419
420// ============================================================================
421// Error Predicates
422// ============================================================================
423
424impl Error {
425 /// Returns `true` if this is a timeout error.
426 #[inline]
427 #[must_use]
428 pub fn is_timeout(&self) -> bool {
429 matches!(
430 self,
431 Self::ConnectionTimeout { .. } | Self::Timeout { .. } | Self::RequestTimeout { .. }
432 )
433 }
434
435 /// Returns `true` if this is an element error.
436 #[inline]
437 #[must_use]
438 pub fn is_element_error(&self) -> bool {
439 matches!(
440 self,
441 Self::ElementNotFound { .. } | Self::StaleElement { .. }
442 )
443 }
444
445 /// Returns `true` if this is a connection error.
446 #[inline]
447 #[must_use]
448 pub fn is_connection_error(&self) -> bool {
449 matches!(
450 self,
451 Self::Connection { .. }
452 | Self::ConnectionTimeout { .. }
453 | Self::ConnectionClosed
454 | Self::WebSocket(_)
455 )
456 }
457
458 /// Returns `true` if this error is recoverable.
459 ///
460 /// Recoverable errors may succeed on retry.
461 #[inline]
462 #[must_use]
463 pub fn is_recoverable(&self) -> bool {
464 matches!(
465 self,
466 Self::ConnectionTimeout { .. }
467 | Self::Timeout { .. }
468 | Self::RequestTimeout { .. }
469 | Self::StaleElement { .. }
470 )
471 }
472}
473
474// ============================================================================
475// Tests
476// ============================================================================
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 use std::io::ErrorKind;
483
484 #[test]
485 fn test_error_display() {
486 let err = Error::connection("failed to connect");
487 assert_eq!(err.to_string(), "Connection failed: failed to connect");
488 }
489
490 #[test]
491 fn test_config_error() {
492 let err = Error::config("missing binary path");
493 assert_eq!(err.to_string(), "Configuration error: missing binary path");
494 }
495
496 #[test]
497 fn test_is_timeout() {
498 let timeout_err = Error::ConnectionTimeout { timeout_ms: 5000 };
499 let other_err = Error::connection("test");
500
501 assert!(timeout_err.is_timeout());
502 assert!(!other_err.is_timeout());
503 }
504
505 #[test]
506 fn test_is_connection_error() {
507 let conn_err = Error::connection("test");
508 let timeout_err = Error::ConnectionTimeout { timeout_ms: 1000 };
509 let closed_err = Error::ConnectionClosed;
510 let other_err = Error::config("test");
511
512 assert!(conn_err.is_connection_error());
513 assert!(timeout_err.is_connection_error());
514 assert!(closed_err.is_connection_error());
515 assert!(!other_err.is_connection_error());
516 }
517
518 #[test]
519 fn test_is_recoverable() {
520 let timeout_err = Error::Timeout {
521 operation: "test".into(),
522 timeout_ms: 1000,
523 };
524 let config_err = Error::config("test");
525
526 assert!(timeout_err.is_recoverable());
527 assert!(!config_err.is_recoverable());
528 }
529
530 #[test]
531 fn test_from_io_error() {
532 let io_err = IoError::new(ErrorKind::NotFound, "file not found");
533 let err: Error = io_err.into();
534 assert!(matches!(err, Error::Io(_)));
535 }
536
537 #[test]
538 fn test_from_json_error() {
539 let json_err = serde_json::from_str::<String>("invalid").unwrap_err();
540 let err: Error = json_err.into();
541 assert!(matches!(err, Error::Json(_)));
542 }
543}