Skip to main content

playwright_rs/protocol/page/
navigation.rs

1use super::{Page, Response};
2use crate::error::{Error, Result};
3use crate::server::channel_owner::ChannelOwner;
4use serde::Deserialize;
5use std::sync::Arc;
6
7/// Navigation: `goto`, history, load state and URL fragments.
8///
9/// ```no_run
10/// # use playwright_rs::Playwright;
11/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
12/// # let page = Playwright::launch().await?.chromium().launch().await?.new_page().await?;
13/// let response = page.goto("https://example.com", None).await?;
14/// assert!(response.is_some_and(|r| r.ok()));
15///
16/// page.goto("https://example.com/about", None).await?;
17/// page.go_back(None).await?;
18/// page.reload(None).await?;
19/// page.wait_for_load_state(None).await?;
20/// # Ok(())
21/// # }
22/// ```
23impl Page {
24    /// Navigates to the specified URL.
25    ///
26    /// Returns `None` when navigating to URLs that don't produce responses (e.g., data URLs,
27    /// about:blank). This matches Playwright's behavior across all language bindings.
28    ///
29    /// # Arguments
30    ///
31    /// * `url` - The URL to navigate to
32    /// * `options` - Optional navigation options (timeout, wait_until)
33    ///
34    /// # Errors
35    ///
36    /// Returns error if:
37    /// - URL is invalid
38    /// - Navigation timeout (default 30s)
39    /// - Network error
40    ///
41    /// See: <https://playwright.dev/docs/api/class-page#page-goto>
42    #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid(), url = %url, status = tracing::field::Empty))]
43    pub async fn goto(
44        &self,
45        url: &str,
46        options: impl Into<Option<GotoOptions>>,
47    ) -> Result<Option<Response>> {
48        let options = options.into();
49        // Inject the page-level navigation timeout when no explicit timeout is given
50        let options = self.with_navigation_timeout(options);
51
52        // Delegate to main frame
53        let frame = self.main_frame().await.map_err(|e| match e {
54            Error::TargetClosed { context, .. } => Error::TargetClosed {
55                target_type: "Page".to_string(),
56                context,
57            },
58            other => other,
59        })?;
60
61        let response = frame.goto(url, Some(options)).await.map_err(|e| match e {
62            Error::TargetClosed { context, .. } => Error::TargetClosed {
63                target_type: "Page".to_string(),
64                context,
65            },
66            other => other,
67        })?;
68
69        if let Some(ref resp) = response {
70            tracing::Span::current().record("status", resp.status());
71        }
72        Ok(response)
73    }
74
75    /// Waits for the required load state to be reached.
76    ///
77    /// This resolves when the page reaches a required load state, `load` by default.
78    /// The navigation must have been committed when this method is called. If the current
79    /// document has already reached the required state, resolves immediately.
80    ///
81    /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-load-state>
82    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
83    pub async fn wait_for_load_state(&self, state: Option<WaitUntil>) -> Result<()> {
84        let frame = self.main_frame().await?;
85        frame.wait_for_load_state(state).await
86    }
87
88    /// Waits for the main frame to navigate to a URL matching the given string or glob pattern.
89    ///
90    /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-url>
91    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), url = %url))]
92    pub async fn wait_for_url(
93        &self,
94        url: &str,
95        options: impl Into<Option<GotoOptions>>,
96    ) -> Result<()> {
97        let options = options.into();
98        let frame = self.main_frame().await?;
99        frame.wait_for_url(url, options).await
100    }
101
102    /// Replace the URL fragment without firing a navigation.
103    ///
104    /// Wraps `history.replaceState(null, '', <pathname+search+#hash>)`.
105    /// A leading `#` on `hash` is optional — both `"foo"` and `"#foo"`
106    /// produce the same result.
107    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
108    pub async fn set_url_fragment(&self, hash: &str) -> Result<()> {
109        let normalized = if hash.starts_with('#') {
110            hash.to_string()
111        } else {
112            format!("#{hash}")
113        };
114        // JSON-encode so quotes / backslashes / control chars in `hash`
115        // don't break the surrounding JS string literal.
116        let json = serde_json::to_string(&normalized).map_err(|e| {
117            crate::error::Error::ProtocolError(format!("serialize url fragment: {e}"))
118        })?;
119        let js =
120            format!("history.replaceState(null, '', location.pathname + location.search + {json})");
121        self.evaluate_expression(&js).await
122    }
123
124    /// Clear the URL fragment without firing a navigation.
125    ///
126    /// Wraps `history.replaceState(null, '', <pathname+search>)`,
127    /// stripping any trailing `#...`. Pairs with
128    /// [`set_url_fragment`](Self::set_url_fragment).
129    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
130    pub async fn clear_url_fragment(&self) -> Result<()> {
131        self.evaluate_expression(
132            "history.replaceState(null, '', location.pathname + location.search)",
133        )
134        .await
135    }
136
137    /// Reloads the current page.
138    ///
139    /// # Arguments
140    ///
141    /// * `options` - Optional reload options (timeout, wait_until)
142    ///
143    /// Returns `None` when reloading pages that don't produce responses (e.g., data URLs,
144    /// about:blank). This matches Playwright's behavior across all language bindings.
145    ///
146    /// See: <https://playwright.dev/docs/api/class-page#page-reload>
147    #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
148    pub async fn reload(
149        &self,
150        options: impl Into<Option<GotoOptions>>,
151    ) -> Result<Option<Response>> {
152        let options = options.into();
153        self.navigate_history("reload", options).await
154    }
155
156    /// Navigates to the previous page in history.
157    ///
158    /// Returns the main resource response. In case of multiple server redirects, the navigation
159    /// will resolve with the response of the last redirect. If can not go back, returns `None`.
160    ///
161    /// See: <https://playwright.dev/docs/api/class-page#page-go-back>
162    #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
163    pub async fn go_back(
164        &self,
165        options: impl Into<Option<GotoOptions>>,
166    ) -> Result<Option<Response>> {
167        let options = options.into();
168        self.navigate_history("goBack", options).await
169    }
170
171    /// Navigates to the next page in history.
172    ///
173    /// Returns the main resource response. In case of multiple server redirects, the navigation
174    /// will resolve with the response of the last redirect. If can not go forward, returns `None`.
175    ///
176    /// See: <https://playwright.dev/docs/api/class-page#page-go-forward>
177    #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
178    pub async fn go_forward(
179        &self,
180        options: impl Into<Option<GotoOptions>>,
181    ) -> Result<Option<Response>> {
182        let options = options.into();
183        self.navigate_history("goForward", options).await
184    }
185
186    /// Shared implementation for reload, go_back and go_forward.
187    async fn navigate_history(
188        &self,
189        method: &str,
190        options: Option<GotoOptions>,
191    ) -> Result<Option<Response>> {
192        // Inject the page-level navigation timeout when no explicit timeout is given
193        let opts = self.with_navigation_timeout(options);
194        let mut params = serde_json::json!({});
195
196        // opts.timeout is always Some(...) because with_navigation_timeout guarantees it
197        if let Some(timeout) = opts.timeout {
198            params["timeout"] = serde_json::json!(timeout.as_millis() as u64);
199        } else {
200            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
201        }
202        if let Some(wait_until) = opts.wait_until {
203            params["waitUntil"] = serde_json::json!(wait_until.as_str());
204        }
205
206        #[derive(Deserialize)]
207        struct NavigationResponse {
208            response: Option<ResponseReference>,
209        }
210
211        #[derive(Deserialize)]
212        struct ResponseReference {
213            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
214            guid: Arc<str>,
215        }
216
217        let result: NavigationResponse = self.channel().send(method, params).await?;
218
219        if let Some(response_ref) = result.response {
220            // The Response's __create__ may arrive just after the response.
221            let response_arc = self
222                .connection()
223                .wait_for_object(&response_ref.guid)
224                .await?;
225
226            let initializer = response_arc.initializer();
227
228            let status = initializer["status"].as_u64().ok_or_else(|| {
229                crate::error::Error::ProtocolError("Response missing status".to_string())
230            })? as u16;
231
232            let headers = initializer["headers"]
233                .as_array()
234                .ok_or_else(|| {
235                    crate::error::Error::ProtocolError("Response missing headers".to_string())
236                })?
237                .iter()
238                .filter_map(|h| {
239                    let name = h["name"].as_str()?;
240                    let value = h["value"].as_str()?;
241                    Some((name.to_string(), value.to_string()))
242                })
243                .collect();
244
245            let response = Response::new(
246                initializer["url"]
247                    .as_str()
248                    .ok_or_else(|| {
249                        crate::error::Error::ProtocolError("Response missing url".to_string())
250                    })?
251                    .to_string(),
252                status,
253                initializer["statusText"].as_str().unwrap_or("").to_string(),
254                headers,
255                Some(response_arc),
256            );
257
258            Ok(Some(response))
259        } else {
260            Ok(None)
261        }
262    }
263
264    /// Returns GotoOptions with the navigation timeout filled in if not already set.
265    ///
266    /// Used internally to ensure the page's configured default navigation timeout
267    /// is used when the caller does not provide an explicit timeout.
268    fn with_navigation_timeout(&self, options: Option<GotoOptions>) -> GotoOptions {
269        let nav_timeout = self.default_navigation_timeout_ms();
270        match options {
271            Some(opts) if opts.timeout.is_some() => opts,
272            Some(mut opts) => {
273                opts.timeout = Some(std::time::Duration::from_millis(nav_timeout as u64));
274                opts
275            }
276            None => GotoOptions {
277                timeout: Some(std::time::Duration::from_millis(nav_timeout as u64)),
278                wait_until: None,
279            },
280        }
281    }
282}
283
284/// Options for page.goto() and page.reload()
285#[derive(Debug, Clone)]
286#[non_exhaustive]
287pub struct GotoOptions {
288    /// Maximum operation time in milliseconds
289    pub timeout: Option<std::time::Duration>,
290    /// When to consider operation succeeded
291    pub wait_until: Option<WaitUntil>,
292}
293
294impl GotoOptions {
295    /// Creates new GotoOptions with default values
296    pub fn new() -> Self {
297        Self {
298            timeout: None,
299            wait_until: None,
300        }
301    }
302
303    /// Sets the timeout
304    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
305        self.timeout = Some(timeout);
306        self
307    }
308
309    /// Sets the wait_until option
310    pub fn wait_until(mut self, wait_until: WaitUntil) -> Self {
311        self.wait_until = Some(wait_until);
312        self
313    }
314}
315
316impl Default for GotoOptions {
317    fn default() -> Self {
318        Self::new()
319    }
320}
321
322/// When to consider navigation succeeded
323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
324#[non_exhaustive]
325pub enum WaitUntil {
326    /// Consider operation to be finished when the `load` event is fired
327    Load,
328    /// Consider operation to be finished when the `DOMContentLoaded` event is fired
329    DomContentLoaded,
330    /// Consider operation to be finished when there are no network connections for at least 500ms
331    NetworkIdle,
332    /// Consider operation to be finished when the commit event is fired
333    Commit,
334}
335
336impl WaitUntil {
337    pub(crate) fn as_str(&self) -> &'static str {
338        match self {
339            WaitUntil::Load => "load",
340            WaitUntil::DomContentLoaded => "domcontentloaded",
341            WaitUntil::NetworkIdle => "networkidle",
342            WaitUntil::Commit => "commit",
343        }
344    }
345}