use thiserror::Error;
#[derive(Debug, Error)]
pub enum NavigationError {
#[error("Page navigated away during operation")]
PageNavigatedAway,
#[error("Navigation was interrupted: {0}")]
Interrupted(String),
#[error("Navigation timed out after {0}ms")]
Timeout(u64),
#[error("Target was detached")]
TargetDetached,
#[error("Execution context was destroyed")]
ExecutionContextDestroyed,
}
const NAVIGATION_ERROR_PATTERNS: &[&str] = &[
"navigat", "detached",
"context was destroyed",
"execution context was destroyed",
"frame was detached",
"target closed",
"page has been closed",
"session closed",
"cannot find context",
"protocol error",
];
const TIMEOUT_ERROR_PATTERNS: &[&str] = &["timed out", "timeout", "exceeded", "waiting for"];
pub fn is_navigation_error(error_message: &str) -> bool {
NAVIGATION_ERROR_PATTERNS.iter().any(|pattern| {
error_message
.to_lowercase()
.contains(&pattern.to_lowercase())
})
}
pub fn is_timeout_error(error_message: &str) -> bool {
TIMEOUT_ERROR_PATTERNS.iter().any(|pattern| {
error_message
.to_lowercase()
.contains(&pattern.to_lowercase())
})
}
pub async fn safe_operation<F, T, E>(operation: F, default: T) -> T
where
F: std::future::Future<Output = Result<T, E>>,
E: std::fmt::Display,
{
match operation.await {
Ok(value) => value,
Err(e) if is_navigation_error(&e.to_string()) => default,
Err(_) => default, }
}
#[derive(Debug)]
pub struct SafeResult<T> {
pub value: T,
pub success: bool,
pub navigation_error: bool,
}
impl<T: Default> SafeResult<T> {
pub fn success(value: T) -> Self {
Self {
value,
success: true,
navigation_error: false,
}
}
pub fn navigation_error() -> Self {
Self {
value: T::default(),
success: false,
navigation_error: true,
}
}
pub fn error(value: T) -> Self {
Self {
value,
success: false,
navigation_error: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_navigation_error_detects_common_patterns() {
assert!(is_navigation_error("Page navigated away"));
assert!(is_navigation_error("Target was detached"));
assert!(is_navigation_error("Execution context was destroyed"));
assert!(is_navigation_error("frame was detached"));
assert!(is_navigation_error("Target closed"));
assert!(is_navigation_error("page has been closed"));
assert!(is_navigation_error(
"Session closed. Most likely the page has been closed."
));
assert!(is_navigation_error("Cannot find context with specified id"));
assert!(is_navigation_error("Protocol error: Target closed"));
}
#[test]
fn is_navigation_error_case_insensitive() {
assert!(is_navigation_error("NAVIGATION ERROR"));
assert!(is_navigation_error("Target Detached"));
assert!(is_navigation_error("CONTEXT WAS DESTROYED"));
}
#[test]
fn is_navigation_error_false_for_other_errors() {
assert!(!is_navigation_error("Element not found"));
assert!(!is_navigation_error("Invalid selector"));
assert!(!is_navigation_error("Network error"));
}
#[test]
fn is_timeout_error_detects_timeout_patterns() {
assert!(is_timeout_error("Operation timed out"));
assert!(is_timeout_error("Timeout exceeded"));
assert!(is_timeout_error("waiting for selector"));
assert!(is_timeout_error("Timeout"));
}
#[test]
fn is_timeout_error_false_for_other_errors() {
assert!(!is_timeout_error("Element not found"));
assert!(!is_timeout_error("Navigation error"));
}
#[test]
fn safe_result_success() {
let result = SafeResult::success(42);
assert_eq!(result.value, 42);
assert!(result.success);
assert!(!result.navigation_error);
}
#[test]
fn safe_result_navigation_error() {
let result: SafeResult<i32> = SafeResult::navigation_error();
assert_eq!(result.value, 0);
assert!(!result.success);
assert!(result.navigation_error);
}
#[test]
fn safe_result_error() {
let result: SafeResult<String> = SafeResult::error("fallback".to_string());
assert_eq!(result.value, "fallback");
assert!(!result.success);
assert!(!result.navigation_error);
}
}