Skip to main content

firefox_webdriver/browser/tab/
navigation.rs

1//! Tab navigation methods.
2
3use tracing::debug;
4
5use crate::error::Result;
6use crate::protocol::{BrowsingContextCommand, Command};
7
8use super::Tab;
9
10// ============================================================================
11// Tab - Navigation
12// ============================================================================
13
14impl Tab {
15    /// Navigates to a URL.
16    ///
17    /// # Arguments
18    ///
19    /// * `url` - The URL to navigate to
20    ///
21    /// # Errors
22    ///
23    /// Returns an error if navigation fails.
24    pub async fn goto(&self, url: &str) -> Result<()> {
25        debug!(url = %url, tab_id = %self.inner.tab_id, "Navigating");
26
27        let command = Command::BrowsingContext(BrowsingContextCommand::Navigate {
28            url: url.to_string(),
29        });
30
31        self.send_command(command).await?;
32        Ok(())
33    }
34
35    /// Loads HTML content directly into the page.
36    ///
37    /// Useful for testing with inline HTML without needing a server.
38    ///
39    /// # Arguments
40    ///
41    /// * `html` - HTML content to load
42    ///
43    /// # Example
44    ///
45    /// ```ignore
46    /// tab.load_html("<html><body><h1>Test</h1></body></html>").await?;
47    /// ```
48    pub async fn load_html(&self, html: &str) -> Result<()> {
49        debug!(tab_id = %self.inner.tab_id, html_len = html.len(), "Loading HTML content");
50
51        let escaped_html = escape_for_template_literal(html);
52
53        let script = format!(
54            r#"(function() {{
55                const html = `{}`;
56                const parser = new DOMParser();
57                const doc = parser.parseFromString(html, 'text/html');
58                const newTitle = doc.querySelector('title');
59                if (newTitle) {{ document.title = newTitle.textContent; }}
60                const newBody = doc.body;
61                if (newBody) {{
62                    document.body.innerHTML = newBody.innerHTML;
63                    for (const attr of newBody.attributes) {{
64                        document.body.setAttribute(attr.name, attr.value);
65                    }}
66                }}
67                const newHead = doc.head;
68                if (newHead) {{
69                    for (const child of newHead.children) {{
70                        if (child.tagName !== 'TITLE') {{
71                            document.head.appendChild(child.cloneNode(true));
72                        }}
73                    }}
74                }}
75            }})();"#,
76            escaped_html
77        );
78
79        self.execute_script(&script).await?;
80        Ok(())
81    }
82
83    /// Reloads the current page.
84    pub async fn reload(&self) -> Result<()> {
85        debug!(tab_id = %self.inner.tab_id, "Reloading page");
86        let command = Command::BrowsingContext(BrowsingContextCommand::Reload);
87        self.send_command(command).await?;
88        Ok(())
89    }
90
91    /// Navigates back in history.
92    pub async fn back(&self) -> Result<()> {
93        debug!(tab_id = %self.inner.tab_id, "Navigating back");
94        let command = Command::BrowsingContext(BrowsingContextCommand::GoBack);
95        self.send_command(command).await?;
96        Ok(())
97    }
98
99    /// Navigates forward in history.
100    pub async fn forward(&self) -> Result<()> {
101        debug!(tab_id = %self.inner.tab_id, "Navigating forward");
102        let command = Command::BrowsingContext(BrowsingContextCommand::GoForward);
103        self.send_command(command).await?;
104        Ok(())
105    }
106
107    /// Gets the current page title.
108    pub async fn get_title(&self) -> Result<String> {
109        debug!(tab_id = %self.inner.tab_id, "Getting page title");
110        let command = Command::BrowsingContext(BrowsingContextCommand::GetTitle);
111        let response = self.send_command(command).await?;
112
113        let title = response
114            .result
115            .as_ref()
116            .and_then(|v| v.get("title"))
117            .and_then(|v| v.as_str())
118            .unwrap_or("")
119            .to_string();
120
121        debug!(tab_id = %self.inner.tab_id, title = %title, "Got page title");
122        Ok(title)
123    }
124
125    /// Gets the current URL.
126    pub async fn get_url(&self) -> Result<String> {
127        debug!(tab_id = %self.inner.tab_id, "Getting page URL");
128        let command = Command::BrowsingContext(BrowsingContextCommand::GetUrl);
129        let response = self.send_command(command).await?;
130
131        let url = response
132            .result
133            .as_ref()
134            .and_then(|v| v.get("url"))
135            .and_then(|v| v.as_str())
136            .unwrap_or("")
137            .to_string();
138
139        debug!(tab_id = %self.inner.tab_id, url = %url, "Got page URL");
140        Ok(url)
141    }
142
143    /// Focuses this tab (makes it active).
144    pub async fn focus(&self) -> Result<()> {
145        debug!(tab_id = %self.inner.tab_id, "Focusing tab");
146        let command = Command::BrowsingContext(BrowsingContextCommand::FocusTab);
147        self.send_command(command).await?;
148        Ok(())
149    }
150
151    /// Focuses the window containing this tab.
152    pub async fn focus_window(&self) -> Result<()> {
153        debug!(tab_id = %self.inner.tab_id, "Focusing window");
154        let command = Command::BrowsingContext(BrowsingContextCommand::FocusWindow);
155        self.send_command(command).await?;
156        Ok(())
157    }
158
159    /// Closes this tab.
160    pub async fn close(&self) -> Result<()> {
161        debug!(tab_id = %self.inner.tab_id, "Closing tab");
162        let command = Command::BrowsingContext(BrowsingContextCommand::CloseTab);
163        self.send_command(command).await?;
164        Ok(())
165    }
166
167    /// Gets the page source HTML.
168    pub async fn get_page_source(&self) -> Result<String> {
169        debug!(tab_id = %self.inner.tab_id, "Getting page source");
170        let result = self
171            .execute_script("return document.documentElement.outerHTML")
172            .await?;
173        Ok(result.as_str().unwrap_or("").to_string())
174    }
175}
176
177// ============================================================================
178// Private Helpers
179// ============================================================================
180
181/// Escapes a string for safe embedding in a JavaScript template literal.
182///
183/// Performs a single pass over the input, replacing `\` with `\\`,
184/// `` ` `` with `` \` ``, and `${` with `\${`.
185fn escape_for_template_literal(s: &str) -> String {
186    let mut out = String::with_capacity(s.len() + s.len() / 8);
187    let mut chars = s.chars().peekable();
188    while let Some(c) = chars.next() {
189        match c {
190            '\\' => out.push_str("\\\\"),
191            '`' => out.push_str("\\`"),
192            '$' if chars.peek() == Some(&'{') => {
193                chars.next(); // consume '{'
194                out.push_str("\\${");
195            }
196            _ => out.push(c),
197        }
198    }
199    out
200}