Skip to main content

browser_commander/core/
navigation.rs

1//! Navigation safety utilities.
2//!
3//! This module provides utilities for handling navigation-related errors
4//! gracefully during browser automation.
5
6use thiserror::Error;
7
8/// Errors related to navigation operations.
9#[derive(Debug, Error)]
10pub enum NavigationError {
11    /// The page was navigated away during an operation.
12    #[error("Page navigated away during operation")]
13    PageNavigatedAway,
14
15    /// Navigation was interrupted.
16    #[error("Navigation was interrupted: {0}")]
17    Interrupted(String),
18
19    /// Navigation timed out.
20    #[error("Navigation timed out after {0}ms")]
21    Timeout(u64),
22
23    /// Target was detached (e.g., tab closed).
24    #[error("Target was detached")]
25    TargetDetached,
26
27    /// Execution context was destroyed.
28    #[error("Execution context was destroyed")]
29    ExecutionContextDestroyed,
30}
31
32/// Common error messages that indicate a navigation error.
33const NAVIGATION_ERROR_PATTERNS: &[&str] = &[
34    "navigat", // Matches "navigation", "navigated", etc.
35    "detached",
36    "context was destroyed",
37    "execution context was destroyed",
38    "frame was detached",
39    "target closed",
40    "page has been closed",
41    "session closed",
42    "cannot find context",
43    "protocol error",
44];
45
46/// Common error messages that indicate a timeout error.
47const TIMEOUT_ERROR_PATTERNS: &[&str] = &["timed out", "timeout", "exceeded", "waiting for"];
48
49/// Check if an error message indicates a navigation error.
50///
51/// Navigation errors are expected during browser automation when pages
52/// navigate away during operations. These errors should generally be
53/// handled gracefully.
54///
55/// # Arguments
56///
57/// * `error_message` - The error message to check
58///
59/// # Returns
60///
61/// `true` if the error appears to be a navigation error
62pub fn is_navigation_error(error_message: &str) -> bool {
63    NAVIGATION_ERROR_PATTERNS.iter().any(|pattern| {
64        error_message
65            .to_lowercase()
66            .contains(&pattern.to_lowercase())
67    })
68}
69
70/// Check if an error message indicates a timeout error.
71///
72/// # Arguments
73///
74/// * `error_message` - The error message to check
75///
76/// # Returns
77///
78/// `true` if the error appears to be a timeout error
79pub fn is_timeout_error(error_message: &str) -> bool {
80    TIMEOUT_ERROR_PATTERNS.iter().any(|pattern| {
81        error_message
82            .to_lowercase()
83            .contains(&pattern.to_lowercase())
84    })
85}
86
87/// Execute an operation with navigation safety.
88///
89/// If the operation fails with a navigation error, returns the default value
90/// instead of propagating the error.
91///
92/// # Arguments
93///
94/// * `operation` - The async operation to execute
95/// * `default` - The value to return on navigation error
96///
97/// # Returns
98///
99/// The operation result, or the default value if a navigation error occurred
100pub async fn safe_operation<F, T, E>(operation: F, default: T) -> T
101where
102    F: std::future::Future<Output = Result<T, E>>,
103    E: std::fmt::Display,
104{
105    match operation.await {
106        Ok(value) => value,
107        Err(e) if is_navigation_error(&e.to_string()) => default,
108        Err(_) => default, // In safe operation mode, return default for any error
109    }
110}
111
112/// Result wrapper that includes navigation error information.
113#[derive(Debug)]
114pub struct SafeResult<T> {
115    /// The operation result value.
116    pub value: T,
117    /// Whether the operation was successful.
118    pub success: bool,
119    /// Whether a navigation error occurred.
120    pub navigation_error: bool,
121}
122
123impl<T: Default> SafeResult<T> {
124    /// Create a successful result.
125    pub fn success(value: T) -> Self {
126        Self {
127            value,
128            success: true,
129            navigation_error: false,
130        }
131    }
132
133    /// Create a result from a navigation error.
134    pub fn navigation_error() -> Self {
135        Self {
136            value: T::default(),
137            success: false,
138            navigation_error: true,
139        }
140    }
141
142    /// Create a result from a general error.
143    pub fn error(value: T) -> Self {
144        Self {
145            value,
146            success: false,
147            navigation_error: false,
148        }
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn is_navigation_error_detects_common_patterns() {
158        assert!(is_navigation_error("Page navigated away"));
159        assert!(is_navigation_error("Target was detached"));
160        assert!(is_navigation_error("Execution context was destroyed"));
161        assert!(is_navigation_error("frame was detached"));
162        assert!(is_navigation_error("Target closed"));
163        assert!(is_navigation_error("page has been closed"));
164        assert!(is_navigation_error(
165            "Session closed. Most likely the page has been closed."
166        ));
167        assert!(is_navigation_error("Cannot find context with specified id"));
168        assert!(is_navigation_error("Protocol error: Target closed"));
169    }
170
171    #[test]
172    fn is_navigation_error_case_insensitive() {
173        assert!(is_navigation_error("NAVIGATION ERROR"));
174        assert!(is_navigation_error("Target Detached"));
175        assert!(is_navigation_error("CONTEXT WAS DESTROYED"));
176    }
177
178    #[test]
179    fn is_navigation_error_false_for_other_errors() {
180        assert!(!is_navigation_error("Element not found"));
181        assert!(!is_navigation_error("Invalid selector"));
182        assert!(!is_navigation_error("Network error"));
183    }
184
185    #[test]
186    fn is_timeout_error_detects_timeout_patterns() {
187        assert!(is_timeout_error("Operation timed out"));
188        assert!(is_timeout_error("Timeout exceeded"));
189        assert!(is_timeout_error("waiting for selector"));
190        assert!(is_timeout_error("Timeout"));
191    }
192
193    #[test]
194    fn is_timeout_error_false_for_other_errors() {
195        assert!(!is_timeout_error("Element not found"));
196        assert!(!is_timeout_error("Navigation error"));
197    }
198
199    #[test]
200    fn safe_result_success() {
201        let result = SafeResult::success(42);
202        assert_eq!(result.value, 42);
203        assert!(result.success);
204        assert!(!result.navigation_error);
205    }
206
207    #[test]
208    fn safe_result_navigation_error() {
209        let result: SafeResult<i32> = SafeResult::navigation_error();
210        assert_eq!(result.value, 0);
211        assert!(!result.success);
212        assert!(result.navigation_error);
213    }
214
215    #[test]
216    fn safe_result_error() {
217        let result: SafeResult<String> = SafeResult::error("fallback".to_string());
218        assert_eq!(result.value, "fallback");
219        assert!(!result.success);
220        assert!(!result.navigation_error);
221    }
222}