browser_commander/browser/
navigation_ops.rs1use crate::core::constants::TIMING;
7use crate::core::engine::{EngineAdapter, EngineError};
8use crate::core::navigation::is_navigation_error;
9use std::time::{Duration, Instant};
10
11#[derive(Debug, Clone)]
13pub struct NavigationOptions {
14 pub wait_until: WaitUntil,
16 pub timeout: Duration,
18 pub wait_for_stable_url_before: bool,
20 pub wait_for_stable_url_after: bool,
22 pub verify: bool,
24 pub verification_timeout: Duration,
26 pub stable_checks: u32,
28 pub check_interval: Duration,
30}
31
32impl Default for NavigationOptions {
33 fn default() -> Self {
34 Self {
35 wait_until: WaitUntil::DomContentLoaded,
36 timeout: TIMING.navigation_timeout,
37 wait_for_stable_url_before: true,
38 wait_for_stable_url_after: true,
39 verify: true,
40 verification_timeout: TIMING.verification_timeout,
41 stable_checks: 3,
42 check_interval: Duration::from_secs(1),
43 }
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
49pub enum WaitUntil {
50 #[default]
52 DomContentLoaded,
53 Load,
55 NetworkIdle,
57}
58
59impl std::fmt::Display for WaitUntil {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 match self {
62 WaitUntil::DomContentLoaded => write!(f, "domcontentloaded"),
63 WaitUntil::Load => write!(f, "load"),
64 WaitUntil::NetworkIdle => write!(f, "networkidle"),
65 }
66 }
67}
68
69#[derive(Debug, Clone)]
71pub struct NavigationVerificationResult {
72 pub verified: bool,
74 pub actual_url: String,
76 pub reason: String,
78 pub attempts: u32,
80}
81
82#[derive(Debug, Clone)]
84pub struct NavigationResult {
85 pub navigated: bool,
87 pub verified: bool,
89 pub actual_url: Option<String>,
91 pub reason: Option<String>,
93}
94
95impl NavigationResult {
96 pub fn success(actual_url: String) -> Self {
98 Self {
99 navigated: true,
100 verified: true,
101 actual_url: Some(actual_url),
102 reason: Some("navigation completed".to_string()),
103 }
104 }
105
106 pub fn interrupted(reason: impl Into<String>) -> Self {
108 Self {
109 navigated: false,
110 verified: false,
111 actual_url: None,
112 reason: Some(reason.into()),
113 }
114 }
115}
116
117pub async fn verify_navigation(
130 adapter: &dyn EngineAdapter,
131 expected_url: Option<&str>,
132 start_url: &str,
133 options: &NavigationOptions,
134) -> Result<NavigationVerificationResult, EngineError> {
135 let start_time = Instant::now();
136 let mut attempts = 0u32;
137
138 while start_time.elapsed() < options.verification_timeout {
139 attempts += 1;
140
141 let actual_url = match adapter.url().await {
142 Ok(url) => url,
143 Err(e) if is_navigation_error(&e.to_string()) => {
144 return Ok(NavigationVerificationResult {
145 verified: false,
146 actual_url: String::new(),
147 reason: "error during verification".to_string(),
148 attempts,
149 });
150 }
151 Err(e) => return Err(e),
152 };
153
154 if let Some(expected) = expected_url {
156 if actual_url == expected {
157 return Ok(NavigationVerificationResult {
158 verified: true,
159 actual_url,
160 reason: "exact URL match".to_string(),
161 attempts,
162 });
163 }
164
165 if actual_url.contains(expected) || actual_url.starts_with(expected) {
166 return Ok(NavigationVerificationResult {
167 verified: true,
168 actual_url,
169 reason: "URL pattern match".to_string(),
170 attempts,
171 });
172 }
173 } else {
174 if actual_url != start_url {
176 return Ok(NavigationVerificationResult {
177 verified: true,
178 actual_url,
179 reason: "URL changed from start".to_string(),
180 attempts,
181 });
182 }
183 }
184
185 tokio::time::sleep(Duration::from_millis(100)).await;
186 }
187
188 let actual_url = adapter.url().await?;
190
191 Ok(NavigationVerificationResult {
192 verified: false,
193 actual_url: actual_url.clone(),
194 reason: format!(
195 "URL mismatch: expected {:?}, got \"{}\"",
196 expected_url, actual_url
197 ),
198 attempts,
199 })
200}
201
202pub async fn wait_for_url_stabilization(
214 adapter: &dyn EngineAdapter,
215 options: &NavigationOptions,
216 _reason: &str,
217) -> Result<bool, EngineError> {
218 let start_time = Instant::now();
219 let mut stable_count = 0u32;
220 let mut last_url = adapter.url().await?;
221
222 while stable_count < options.stable_checks {
223 if start_time.elapsed() > options.timeout {
225 return Ok(false);
226 }
227
228 tokio::time::sleep(options.check_interval).await;
229
230 let current_url = adapter.url().await?;
231
232 if current_url == last_url {
233 stable_count += 1;
234 } else {
235 stable_count = 0;
236 last_url = current_url;
237 }
238 }
239
240 Ok(true)
241}
242
243pub async fn goto(
255 adapter: &dyn EngineAdapter,
256 url: &str,
257 options: &NavigationOptions,
258) -> Result<NavigationResult, EngineError> {
259 let start_url = adapter.url().await?;
260
261 if options.wait_for_stable_url_before {
263 wait_for_url_stabilization(adapter, options, "before navigation").await?;
264 }
265
266 match adapter.goto(url).await {
268 Ok(_) => {}
269 Err(e) if is_navigation_error(&e.to_string()) => {
270 return Ok(NavigationResult::interrupted("navigation was interrupted"));
271 }
272 Err(e) => return Err(e),
273 }
274
275 if options.wait_for_stable_url_after {
277 wait_for_url_stabilization(adapter, options, "after navigation").await?;
278 }
279
280 if options.verify {
282 let verification = verify_navigation(adapter, Some(url), &start_url, options).await?;
283
284 return Ok(NavigationResult {
285 navigated: true,
286 verified: verification.verified,
287 actual_url: Some(verification.actual_url),
288 reason: Some(verification.reason),
289 });
290 }
291
292 let actual_url = adapter.url().await?;
293 Ok(NavigationResult::success(actual_url))
294}
295
296pub async fn wait_for_navigation(
307 adapter: &dyn EngineAdapter,
308 timeout_ms: u64,
309) -> Result<bool, EngineError> {
310 match adapter.wait_for_navigation(timeout_ms).await {
311 Ok(_) => Ok(true),
312 Err(e) if is_navigation_error(&e.to_string()) => Ok(false),
313 Err(e) => Err(e),
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn navigation_options_default() {
323 let options = NavigationOptions::default();
324 assert_eq!(options.wait_until, WaitUntil::DomContentLoaded);
325 assert!(options.wait_for_stable_url_before);
326 assert!(options.wait_for_stable_url_after);
327 assert!(options.verify);
328 assert_eq!(options.stable_checks, 3);
329 }
330
331 #[test]
332 fn wait_until_display() {
333 assert_eq!(WaitUntil::DomContentLoaded.to_string(), "domcontentloaded");
334 assert_eq!(WaitUntil::Load.to_string(), "load");
335 assert_eq!(WaitUntil::NetworkIdle.to_string(), "networkidle");
336 }
337
338 #[test]
339 fn navigation_result_success() {
340 let result = NavigationResult::success("https://example.com".to_string());
341 assert!(result.navigated);
342 assert!(result.verified);
343 assert_eq!(result.actual_url, Some("https://example.com".to_string()));
344 }
345
346 #[test]
347 fn navigation_result_interrupted() {
348 let result = NavigationResult::interrupted("page was closed");
349 assert!(!result.navigated);
350 assert!(!result.verified);
351 assert!(result.actual_url.is_none());
352 assert_eq!(result.reason, Some("page was closed".to_string()));
353 }
354}