viewpoint_core/page/frame/
navigation.rs

1//! Frame navigation and load state operations.
2
3use std::time::Duration;
4
5use tracing::{debug, info, instrument};
6use viewpoint_cdp::protocol::page::NavigateParams;
7
8use super::Frame;
9use crate::error::NavigationError;
10use crate::wait::{DocumentLoadState, LoadStateWaiter};
11
12/// Default navigation timeout.
13pub(super) const DEFAULT_NAVIGATION_TIMEOUT: Duration = Duration::from_secs(30);
14
15impl Frame {
16    /// Navigate the frame to a URL.
17    ///
18    /// # Errors
19    ///
20    /// Returns an error if the frame is detached or navigation fails.
21    #[instrument(level = "info", skip(self), fields(frame_id = %self.id, url = %url))]
22    pub async fn goto(&self, url: &str) -> Result<(), NavigationError> {
23        self.goto_with_options(url, DocumentLoadState::Load, DEFAULT_NAVIGATION_TIMEOUT)
24            .await
25    }
26
27    /// Navigate the frame to a URL with options.
28    ///
29    /// # Errors
30    ///
31    /// Returns an error if the frame is detached or navigation fails.
32    #[instrument(level = "info", skip(self), fields(frame_id = %self.id, url = %url, wait_until = ?wait_until))]
33    pub async fn goto_with_options(
34        &self,
35        url: &str,
36        wait_until: DocumentLoadState,
37        timeout: Duration,
38    ) -> Result<(), NavigationError> {
39        if self.is_detached() {
40            return Err(NavigationError::Cancelled);
41        }
42
43        info!("Navigating frame to URL");
44
45        // Create a load state waiter
46        let event_rx = self.connection.subscribe_events();
47        let mut waiter = LoadStateWaiter::new(event_rx, self.session_id.clone(), self.id.clone());
48
49        // Send navigation command with frame_id
50        debug!("Sending Page.navigate command for frame");
51        let result: viewpoint_cdp::protocol::page::NavigateResult = self
52            .connection
53            .send_command(
54                "Page.navigate",
55                Some(NavigateParams {
56                    url: url.to_string(),
57                    referrer: None,
58                    transition_type: None,
59                    frame_id: Some(self.id.clone()),
60                }),
61                Some(&self.session_id),
62            )
63            .await?;
64
65        debug!(frame_id = %result.frame_id, "Page.navigate completed for frame");
66
67        // Check for navigation errors
68        if let Some(error_text) = result.error_text {
69            return Err(NavigationError::NetworkError(error_text));
70        }
71
72        // Mark commit as received
73        waiter.set_commit_received().await;
74
75        // Wait for the target load state
76        debug!(wait_until = ?wait_until, "Waiting for load state");
77        waiter
78            .wait_for_load_state_with_timeout(wait_until, timeout)
79            .await?;
80
81        // Update the frame's URL
82        self.set_url(url.to_string());
83
84        info!(frame_id = %self.id, "Frame navigation completed");
85        Ok(())
86    }
87
88    /// Wait for the frame to reach a specific load state.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error if the wait times out or the frame is detached.
93    #[instrument(level = "debug", skip(self), fields(frame_id = %self.id, state = ?state))]
94    pub async fn wait_for_load_state(
95        &self,
96        state: DocumentLoadState,
97    ) -> Result<(), NavigationError> {
98        self.wait_for_load_state_with_timeout(state, DEFAULT_NAVIGATION_TIMEOUT)
99            .await
100    }
101
102    /// Wait for the frame to reach a specific load state with timeout.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if the wait times out or the frame is detached.
107    #[instrument(level = "debug", skip(self), fields(frame_id = %self.id, state = ?state, timeout_ms = timeout.as_millis()))]
108    pub async fn wait_for_load_state_with_timeout(
109        &self,
110        state: DocumentLoadState,
111        timeout: Duration,
112    ) -> Result<(), NavigationError> {
113        if self.is_detached() {
114            return Err(NavigationError::Cancelled);
115        }
116
117        let event_rx = self.connection.subscribe_events();
118        let mut waiter = LoadStateWaiter::new(event_rx, self.session_id.clone(), self.id.clone());
119
120        // Assume commit already happened for existing frames
121        waiter.set_commit_received().await;
122
123        waiter
124            .wait_for_load_state_with_timeout(state, timeout)
125            .await?;
126
127        debug!("Frame reached load state {:?}", state);
128        Ok(())
129    }
130}