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}