Skip to main content

browser_commander/utilities/
wait.rs

1//! Wait/sleep utilities for browser automation.
2//!
3//! This module provides utilities for waiting and sleeping
4//! with optional abort signal support.
5
6use crate::core::engine::{EngineAdapter, EngineError};
7use crate::core::navigation::is_navigation_error;
8use std::time::Duration;
9use tokio_util::sync::CancellationToken;
10
11/// Result of a wait operation.
12#[derive(Debug, Clone)]
13pub struct WaitResult {
14    /// Whether the wait completed normally.
15    pub completed: bool,
16    /// Whether the wait was aborted.
17    pub aborted: bool,
18}
19
20impl WaitResult {
21    /// Create a completed result.
22    pub fn completed() -> Self {
23        Self {
24            completed: true,
25            aborted: false,
26        }
27    }
28
29    /// Create an aborted result.
30    pub fn aborted() -> Self {
31        Self {
32            completed: false,
33            aborted: true,
34        }
35    }
36}
37
38/// Wait for a specified duration.
39///
40/// # Arguments
41///
42/// * `duration` - How long to wait
43/// * `reason` - Reason for waiting (for logging)
44///
45/// # Returns
46///
47/// The wait result
48pub async fn wait(duration: Duration, _reason: Option<&str>) -> WaitResult {
49    tokio::time::sleep(duration).await;
50    WaitResult::completed()
51}
52
53/// Wait for a specified duration with abort support.
54///
55/// # Arguments
56///
57/// * `duration` - How long to wait
58/// * `cancel_token` - Cancellation token to abort the wait
59/// * `reason` - Reason for waiting (for logging)
60///
61/// # Returns
62///
63/// The wait result
64pub async fn wait_with_cancel(
65    duration: Duration,
66    cancel_token: &CancellationToken,
67    _reason: Option<&str>,
68) -> WaitResult {
69    // Check if already cancelled
70    if cancel_token.is_cancelled() {
71        return WaitResult::aborted();
72    }
73
74    tokio::select! {
75        _ = tokio::time::sleep(duration) => {
76            WaitResult::completed()
77        }
78        _ = cancel_token.cancelled() => {
79            WaitResult::aborted()
80        }
81    }
82}
83
84/// Evaluate JavaScript in the page context.
85///
86/// # Arguments
87///
88/// * `adapter` - The engine adapter to use
89/// * `script` - The JavaScript to evaluate
90///
91/// # Returns
92///
93/// The result of the evaluation
94pub async fn evaluate(
95    adapter: &dyn EngineAdapter,
96    script: &str,
97) -> Result<serde_json::Value, EngineError> {
98    adapter.evaluate(script).await
99}
100
101/// Safe evaluate that catches navigation errors and returns a default value.
102///
103/// # Arguments
104///
105/// * `adapter` - The engine adapter to use
106/// * `script` - The JavaScript to evaluate
107/// * `default` - Default value to return on navigation error
108///
109/// # Returns
110///
111/// A result indicating success/failure and the value
112pub async fn safe_evaluate(
113    adapter: &dyn EngineAdapter,
114    script: &str,
115    default: serde_json::Value,
116) -> SafeEvaluateResult {
117    match adapter.evaluate(script).await {
118        Ok(value) => SafeEvaluateResult {
119            success: true,
120            value,
121            navigation_error: false,
122        },
123        Err(e) if is_navigation_error(&e.to_string()) => SafeEvaluateResult {
124            success: false,
125            value: default,
126            navigation_error: true,
127        },
128        Err(_) => SafeEvaluateResult {
129            success: false,
130            value: default,
131            navigation_error: false,
132        },
133    }
134}
135
136/// Result of a safe evaluate operation.
137#[derive(Debug, Clone)]
138pub struct SafeEvaluateResult {
139    /// Whether the evaluation succeeded.
140    pub success: bool,
141    /// The value (actual result or default).
142    pub value: serde_json::Value,
143    /// Whether a navigation error occurred.
144    pub navigation_error: bool,
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn wait_result_completed() {
153        let result = WaitResult::completed();
154        assert!(result.completed);
155        assert!(!result.aborted);
156    }
157
158    #[test]
159    fn wait_result_aborted() {
160        let result = WaitResult::aborted();
161        assert!(!result.completed);
162        assert!(result.aborted);
163    }
164
165    #[tokio::test]
166    async fn wait_completes() {
167        let start = std::time::Instant::now();
168        let result = wait(Duration::from_millis(50), Some("test wait")).await;
169        let elapsed = start.elapsed();
170
171        assert!(result.completed);
172        assert!(!result.aborted);
173        assert!(elapsed >= Duration::from_millis(50));
174    }
175
176    #[tokio::test]
177    async fn wait_with_cancel_completes_normally() {
178        let token = CancellationToken::new();
179        let result = wait_with_cancel(Duration::from_millis(10), &token, Some("test wait")).await;
180
181        assert!(result.completed);
182        assert!(!result.aborted);
183    }
184
185    #[tokio::test]
186    async fn wait_with_cancel_aborts_on_cancel() {
187        let token = CancellationToken::new();
188
189        // Cancel immediately
190        token.cancel();
191
192        let result = wait_with_cancel(
193            Duration::from_secs(10), // Long duration that would be cancelled
194            &token,
195            Some("test wait"),
196        )
197        .await;
198
199        assert!(!result.completed);
200        assert!(result.aborted);
201    }
202
203    #[tokio::test]
204    async fn wait_with_cancel_aborts_during_wait() {
205        let token = CancellationToken::new();
206        let token_clone = token.clone();
207
208        // Spawn task to cancel after a short delay
209        tokio::spawn(async move {
210            tokio::time::sleep(Duration::from_millis(50)).await;
211            token_clone.cancel();
212        });
213
214        let start = std::time::Instant::now();
215        let result = wait_with_cancel(
216            Duration::from_secs(10), // Long duration that should be cancelled
217            &token,
218            Some("test wait"),
219        )
220        .await;
221        let elapsed = start.elapsed();
222
223        assert!(!result.completed);
224        assert!(result.aborted);
225        // Should have been cancelled quickly, not after 10 seconds
226        assert!(elapsed < Duration::from_secs(1));
227    }
228
229    #[test]
230    fn safe_evaluate_result_success() {
231        let result = SafeEvaluateResult {
232            success: true,
233            value: serde_json::json!(42),
234            navigation_error: false,
235        };
236        assert!(result.success);
237        assert!(!result.navigation_error);
238        assert_eq!(result.value, serde_json::json!(42));
239    }
240
241    #[test]
242    fn safe_evaluate_result_navigation_error() {
243        let result = SafeEvaluateResult {
244            success: false,
245            value: serde_json::Value::Null,
246            navigation_error: true,
247        };
248        assert!(!result.success);
249        assert!(result.navigation_error);
250        assert!(result.value.is_null());
251    }
252}