Skip to main content

glass/browser/session/
navigate.rs

1//! Page navigation and JavaScript evaluation.
2//!
3//! Navigates the active page target to a URL with configurable timeouts,
4//! and evaluates JavaScript expressions in the page context.
5
6use super::*;
7
8impl BrowserSession {
9    /// Return the current page's URL, title, and ready state.
10    ///
11    /// Evaluates `location.href`, `document.title`, and `document.readyState`
12    /// in the active page context. Includes the current target and frame IDs.
13    pub async fn page_info(&self) -> BrowserResult<PageInfo> {
14        self.cdp.with_current_route(async {
15        let raw = self
16            .cdp
17            .evaluate(
18                "JSON.stringify({url: location.href, title: document.title, ready_state: document.readyState})",
19            )
20            .await?;
21        let value = runtime_value(&raw)?;
22        let json = value
23            .as_str()
24            .ok_or("document state evaluation returned a non-string value")?;
25                let mut page: PageInfo = serde_json::from_str(json)?;
26                (page.target_id, page.frame_id) = self.route_identity().await?;
27                Ok(page)
28        }).await
29    }
30
31    /// Navigate the active target to a URL with a 20-second deadline.
32    ///
33    /// Waits for the `Page.loadEventFired` lifecycle event before returning.
34    /// The URL is normalized and validated against the active policy.
35    pub async fn navigate(&self, url: &str) -> BrowserResult<PageInfo> {
36        self.navigate_with_deadline(url, Duration::from_secs(20))
37            .await
38    }
39
40    /// Navigate to a URL with an explicit deadline.
41    ///
42    /// Like [`navigate`](Self::navigate), but with a caller-specified timeout.
43    /// `deadline` must be between 1 ms and 30 seconds.
44    pub async fn navigate_with_deadline(
45        &self,
46        url: &str,
47        deadline: Duration,
48    ) -> BrowserResult<PageInfo> {
49        self.cdp
50            .with_current_target_route(async {
51                validate_wait_deadline(deadline)?;
52                let url = normalize_url(url);
53                self.policy.require_url(&url).await?;
54                self.enforce_polite_navigation(&url).await?;
55                if let Some(interception) = &self.policy_interception
56                    && let Some(error) = interception.take_denial().await
57                {
58                    return Err(error.into());
59                }
60                let result = async {
61                    let mut events = self.cdp.subscribe_events();
62                    let started = tokio::time::Instant::now();
63                    let navigation = tokio::time::timeout(deadline, self.cdp.navigate(&url))
64                        .await
65                        .map_err(|_| {
66                            wait_timeout("lifecycle", deadline, "navigate_command_pending")
67                        })??;
68                    if let Some(frame_id) = navigation.frame_id.as_deref() {
69                        validate_topology_id(frame_id)?;
70                        self.topology.lock().await.active_frame_id = Some(frame_id.to_string());
71                        self.cdp
72                            .set_active_frame_context(Some(frame_id.to_string()), None);
73                    }
74                    let remaining = deadline.saturating_sub(started.elapsed());
75                    self.wait_loop(
76                        WaitCondition::Lifecycle("complete".to_string()),
77                        remaining,
78                        deadline,
79                        &mut events,
80                        true,
81                    )
82                    .await?;
83                    let remaining = deadline.saturating_sub(started.elapsed());
84                    let main_frame = self
85                        .list_frames()
86                        .await?
87                        .into_iter()
88                        .find(|frame| frame.parent_id.is_none())
89                        .ok_or("navigated target returned no main frame")?;
90                    self.select_frame(&main_frame.id).await?;
91                    let page = tokio::time::timeout(remaining, self.page_info())
92                        .await
93                        .map_err(|_| wait_timeout("lifecycle", deadline, "page_info_pending"))??;
94                    self.invalidate_observation();
95                    self.record_audit("navigate", url);
96                    Ok(page)
97                }
98                .await;
99                if let Some(error) = match &self.policy_interception {
100                    Some(interception) => interception.take_denial().await,
101                    None => None,
102                } {
103                    return Err(error.into());
104                }
105                result
106            })
107            .await
108    }
109
110    /// Navigate only when the supplied observation revision is still current.
111    /// This is the revision-safe counterpart to the compatibility-preserving
112    /// [`navigate_with_deadline`](Self::navigate_with_deadline) API.
113    pub async fn navigate_with_revision(
114        &self,
115        url: &str,
116        deadline: Duration,
117        expected_revision: u64,
118    ) -> BrowserResult<NavigationOutcome> {
119        self.require_expected_revision(Some(expected_revision))?;
120        let previous_revision = self.page_revision.load(Ordering::Relaxed);
121        let before = self.page_info().await.ok();
122        let page = self.navigate_with_deadline(url, deadline).await?;
123        let current_revision = self.page_revision.load(Ordering::Relaxed);
124        Ok(NavigationOutcome {
125            status: ActionStatus::Succeeded,
126            action: ActionKind::Navigate,
127            execution_id: self.next_execution_id(),
128            verification: ActionVerificationEvidence {
129                revision_delta: current_revision.saturating_sub(previous_revision),
130                url_changed: before.as_ref().is_some_and(|before| before.url != page.url),
131                title_changed: before
132                    .as_ref()
133                    .is_some_and(|before| before.title != page.title),
134                target_changed: before
135                    .as_ref()
136                    .is_some_and(|before| before.target_id != page.target_id),
137                frame_changed: before
138                    .as_ref()
139                    .is_some_and(|before| before.frame_id != page.frame_id),
140                ..ActionVerificationEvidence::default()
141            },
142            page,
143            previous_revision,
144            current_revision,
145        })
146    }
147}