Skip to main content

headless_engine/browser/
tab.rs

1use crate::browser::builder::BrowserBuilder;
2use crate::dom::{DomTree, FormInfo, InteractiveElement, LinkInfo, PageObservation, SearchResults};
3use crate::js::context::JsRuntime;
4use crate::network::client::{FetchResult, NetworkClient};
5use crate::network::fingerprint::DeviceProfile;
6use anyhow::{Context, Result};
7use serde::{Deserialize, Serialize};
8use crate::google::{GoogleEndpoints, GoogleParser, GoogleSearchResult, GenericGoogleResult, GoogleAutocompleteResult};#[derive(Debug, Serialize, Deserialize)]
9pub struct NavigationReport {
10    pub status: u16,
11    pub requested_url: String,
12    pub final_url: String,
13    pub page_title: String,
14    pub is_captcha_detected: bool,
15    pub html_bytes: usize,
16}
17
18pub struct BrowserTab {
19    network: NetworkClient,
20    dom: Option<DomTree>,
21    js: JsRuntime,
22    pub current_url: Option<String>,
23}
24
25impl BrowserTab {
26    pub fn new() -> Result<Self> {
27        Self::with_profile(DeviceProfile::ChromeWindows)
28    }
29
30    pub fn builder() -> BrowserBuilder {
31        BrowserBuilder::new()
32    }
33
34    pub fn with_profile(profile: DeviceProfile) -> Result<Self> {
35        let network = NetworkClient::with_profile(profile)?;
36        Self::from_network(network)
37    }
38
39    pub fn from_network(network: NetworkClient) -> Result<Self> {
40        let js = JsRuntime::with_fingerprint(&network.fingerprint)?;
41        Ok(Self {
42            network,
43            dom: None,
44            js,
45            current_url: None,
46        })
47    }
48
49    pub fn profile(&self) -> DeviceProfile {
50        self.network.profile
51    }
52
53    pub fn set_profile(&mut self, profile: DeviceProfile) -> Result<()> {
54        self.network.set_profile(profile)?;
55        self.js = JsRuntime::with_fingerprint(&self.network.fingerprint)?;
56        Ok(())
57    }
58
59    pub async fn navigate(&mut self, url: &str) -> Result<NavigationReport> {
60        let fetch_result: FetchResult = self.network.fetch(url).await?;
61        let dom = DomTree::parse(&fetch_result.html)?;
62
63        let search_results = dom.parse_google_search_results();
64        let page_title = search_results.page_title.clone();
65        let html_bytes = fetch_result.html.len();
66
67        let _ = self
68            .js
69            .update_page_state(&fetch_result.final_url, &page_title);
70
71        self.dom = Some(dom);
72        self.current_url = Some(fetch_result.final_url.clone());
73
74        Ok(NavigationReport {
75            status: fetch_result.status,
76            requested_url: url.to_string(),
77            final_url: fetch_result.final_url,
78            page_title,
79            is_captcha_detected: fetch_result.is_captcha_detected,
80            html_bytes,
81        })
82    }
83
84    /// Default Google Search with automated query encoding and mode routing
85    pub async fn search(&mut self, query: &str) -> Result<NavigationReport> {
86        self.search_google(query, None).await
87    }
88
89    /// Search Google with specific query modes (e.g. "ai" for udm=50, "web" for udm=14, "images" for udm=2, "news")
90    pub async fn search_google(&mut self, query: &str, mode: Option<&str>) -> Result<NavigationReport> {
91        let encoded: String = query
92            .chars()
93            .map(|c| match c {
94                ' ' => "+".to_string(),
95                'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' | '~' => c.to_string(),
96                _ => format!("%{:02X}", c as u8),
97            })
98            .collect();
99
100        let url = match mode {
101            Some("ai") | Some("udm50") => format!("https://www.google.com/search?q={}&udm=50", encoded),
102            Some("web") | Some("udm14") => format!("https://www.google.com/search?q={}&udm=14", encoded),
103            Some("images") | Some("udm2") => format!("https://www.google.com/search?q={}&udm=2", encoded),
104            Some("news") => format!("https://www.google.com/search?q={}&tbm=nws", encoded),
105            _ => format!("https://www.google.com/search?q={}", encoded),
106        };
107        self.navigate(&url).await
108    }
109
110    pub async fn google_search(&mut self, query: &str) -> Result<GoogleSearchResult> {
111        let url = GoogleEndpoints::search(query);
112        let nav = self.navigate(&url).await?;
113        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
114        Ok(GoogleParser::parse_search_results(&html, &nav.final_url))
115    }
116
117    pub async fn google_web_search(&mut self, query: &str) -> Result<GoogleSearchResult> {
118        let url = GoogleEndpoints::web_search(query);
119        let nav = self.navigate(&url).await?;
120        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
121        Ok(GoogleParser::parse_search_results(&html, &nav.final_url))
122    }
123
124    pub async fn google_image_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
125        let url = GoogleEndpoints::image_search(query);
126        let nav = self.navigate(&url).await?;
127        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
128        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
129    }
130
131    pub async fn google_video_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
132        let url = GoogleEndpoints::video_search(query);
133        let nav = self.navigate(&url).await?;
134        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
135        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
136    }
137
138    pub async fn google_short_video_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
139        let url = GoogleEndpoints::short_video_search(query);
140        let nav = self.navigate(&url).await?;
141        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
142        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
143    }
144
145    pub async fn google_news_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
146        let url = GoogleEndpoints::news_search(query);
147        let nav = self.navigate(&url).await?;
148        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
149        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
150    }
151
152    pub async fn google_forum_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
153        let url = GoogleEndpoints::forum_search(query);
154        let nav = self.navigate(&url).await?;
155        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
156        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
157    }
158
159    pub async fn google_shopping_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
160        let url = GoogleEndpoints::shopping_search(query);
161        let nav = self.navigate(&url).await?;
162        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
163        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
164    }
165
166    pub async fn google_product_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
167        let url = GoogleEndpoints::product_search(query);
168        let nav = self.navigate(&url).await?;
169        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
170        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
171    }
172
173    pub async fn google_books_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
174        let url = GoogleEndpoints::books_search(query);
175        let nav = self.navigate(&url).await?;
176        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
177        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
178    }
179
180    pub async fn google_autocomplete(&mut self, query: &str) -> Result<GoogleAutocompleteResult> {
181        let url = GoogleEndpoints::autocomplete(query);
182        let fetch_result = self.network.fetch(&url).await?;
183        Ok(GoogleParser::parse_autocomplete(&fetch_result.html))
184    }
185
186    pub async fn google_ai_overview(&mut self, query: &str) -> Result<GenericGoogleResult> {
187        let url = GoogleEndpoints::ai_overview(query);
188        let nav = self.navigate(&url).await?;
189        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
190        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
191    }
192
193    pub async fn google_ai_mode(&mut self, query: &str) -> Result<GenericGoogleResult> {
194        let url = GoogleEndpoints::ai_mode(query);
195        let nav = self.navigate(&url).await?;
196        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
197        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
198    }
199
200    pub async fn google_scholar_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
201        let url = GoogleEndpoints::scholar_search(query);
202        let nav = self.navigate(&url).await?;
203        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
204        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
205    }
206
207    pub async fn google_patents_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
208        let url = GoogleEndpoints::patents_search(query);
209        let nav = self.navigate(&url).await?;
210        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
211        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
212    }
213
214    pub async fn google_maps_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
215        let url = GoogleEndpoints::maps_search(query);
216        let nav = self.navigate(&url).await?;
217        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
218        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
219    }
220
221    pub async fn google_finance_quote(&mut self, ticker: &str) -> Result<GenericGoogleResult> {
222        let url = GoogleEndpoints::finance_quote(ticker);
223        let nav = self.navigate(&url).await?;
224        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
225        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
226    }
227
228    pub async fn google_trends_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
229        let url = GoogleEndpoints::trends_search(query);
230        let nav = self.navigate(&url).await?;
231        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
232        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
233    }
234
235    pub async fn google_flights_search(&mut self, origin: &str, dest: &str) -> Result<GenericGoogleResult> {
236        let url = GoogleEndpoints::flights_search(origin, dest);
237        let nav = self.navigate(&url).await?;
238        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
239        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
240    }
241
242    pub async fn google_hotels_search(&mut self, location: &str) -> Result<GenericGoogleResult> {
243        let url = GoogleEndpoints::hotels_search(location);
244        let nav = self.navigate(&url).await?;
245        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
246        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
247    }
248
249    pub async fn google_travel_explore(&mut self, destination: &str) -> Result<GenericGoogleResult> {
250        let url = GoogleEndpoints::travel_explore(destination);
251        let nav = self.navigate(&url).await?;
252        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
253        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
254    }
255
256    pub async fn youtube_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
257        let url = GoogleEndpoints::youtube_search(query);
258        let nav = self.navigate(&url).await?;
259        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
260        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
261    }
262
263    pub async fn youtube_shorts_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
264        let url = GoogleEndpoints::youtube_shorts_search(query);
265        let nav = self.navigate(&url).await?;
266        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
267        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
268    }
269
270    pub async fn youtube_video(&mut self, video_id: &str) -> Result<GenericGoogleResult> {
271        let url = GoogleEndpoints::youtube_video(video_id);
272        let nav = self.navigate(&url).await?;
273        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
274        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
275    }
276
277    pub async fn youtube_channel(&mut self, channel: &str) -> Result<GenericGoogleResult> {
278        let url = GoogleEndpoints::youtube_channel(channel);
279        let nav = self.navigate(&url).await?;
280        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
281        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
282    }
283
284    pub async fn youtube_playlist(&mut self, playlist_id: &str) -> Result<GenericGoogleResult> {
285        let url = GoogleEndpoints::youtube_playlist(playlist_id);
286        let nav = self.navigate(&url).await?;
287        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
288        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
289    }
290
291    pub async fn google_lens_visual_matches(&mut self, image_url: &str) -> Result<GenericGoogleResult> {
292        let url = GoogleEndpoints::lens_visual_matches(image_url);
293        let nav = self.navigate(&url).await?;
294        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
295        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
296    }
297
298    pub async fn google_lens_exact_matches(&mut self, image_url: &str) -> Result<GenericGoogleResult> {
299        let url = GoogleEndpoints::lens_exact_matches(image_url);
300        let nav = self.navigate(&url).await?;
301        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
302        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
303    }
304
305    pub async fn google_lens_products(&mut self, image_url: &str) -> Result<GenericGoogleResult> {
306        let url = GoogleEndpoints::lens_products(image_url);
307        let nav = self.navigate(&url).await?;
308        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
309        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
310    }
311
312    pub async fn google_lens_about_image(&mut self, image_url: &str) -> Result<GenericGoogleResult> {
313        let url = GoogleEndpoints::lens_about_image(image_url);
314        let nav = self.navigate(&url).await?;
315        let html = self.dom.as_ref().map(|d| d.raw_content.clone()).unwrap_or_default();
316        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
317    }
318
319    pub fn google_capabilities(&self) -> Vec<&'static str> {
320        vec![
321            "google_search", "google_web_search", "google_image_search", "google_video_search",
322            "google_short_video_search", "google_news_search", "google_forum_search", "google_shopping_search",
323            "google_product_search", "google_books_search", "google_autocomplete", "google_ai_overview",
324            "google_ai_mode", "google_scholar_search", "google_patents_search", "google_maps_search",
325            "google_finance_quote", "google_trends_search", "google_flights_search", "google_hotels_search",
326            "google_travel_explore", "youtube_search", "youtube_shorts_search", "youtube_video",
327            "youtube_channel", "youtube_playlist", "google_lens_visual_matches", "google_lens_exact_matches",
328            "google_lens_products", "google_lens_about_image", "google_capabilities"
329        ]
330    }
331
332    pub fn set_content(&mut self, html: &str, url: Option<&str>) -> Result<NavigationReport> {
333        let dom = DomTree::parse(html)?;
334        let search_results = dom.parse_google_search_results();
335        let page_title = search_results.page_title.clone();
336        let final_url = url.unwrap_or("about:blank").to_string();
337        let html_bytes = html.len();
338
339        let _ = self.js.update_page_state(&final_url, &page_title);
340
341        self.dom = Some(dom);
342        self.current_url = Some(final_url.clone());
343
344        Ok(NavigationReport {
345            status: 200,
346            requested_url: final_url.clone(),
347            final_url,
348            page_title,
349            is_captcha_detected: search_results.is_captcha_detected,
350            html_bytes,
351        })
352    }
353
354    pub fn observe(&self) -> Option<PageObservation> {
355        let dom = self.dom.as_ref()?;
356        let url = self.current_url.clone().unwrap_or_default();
357        let results = dom.parse_google_search_results();
358        let elements = dom.extract_interactive_elements(Some(&url));
359
360        let mut tree_lines = Vec::new();
361        for el in &elements {
362            tree_lines.push(el.to_agent_string());
363        }
364
365        let agent_tree_text = tree_lines.join("\n");
366        let content_summary_markdown = dom.extract_markdown(None, Some(&url));
367
368        Some(PageObservation {
369            url,
370            title: results.page_title,
371            is_captcha_detected: results.is_captcha_detected,
372            interactive_elements: elements,
373            agent_tree_text,
374            content_summary_markdown,
375        })
376    }
377
378    pub fn evaluate_js(&mut self, code: &str) -> Result<String> {
379        self.js.evaluate(code)
380    }
381
382    pub fn extract_dom(&self, selector: Option<&str>) -> Option<String> {
383        self.dom.as_ref().and_then(|d| d.extract(selector))
384    }
385
386    pub fn extract_markdown(&self, selector: Option<&str>) -> Option<String> {
387        self.dom
388            .as_ref()
389            .map(|d| d.extract_markdown(selector, self.current_url.as_deref()))
390    }
391
392    pub fn extract_interactive_elements(&self) -> Vec<InteractiveElement> {
393        self.dom
394            .as_ref()
395            .map(|d| d.extract_interactive_elements(self.current_url.as_deref()))
396            .unwrap_or_default()
397    }
398
399    pub fn extract_links(&self) -> Vec<LinkInfo> {
400        self.dom
401            .as_ref()
402            .map(|d| d.extract_links(self.current_url.as_deref()))
403            .unwrap_or_default()
404    }
405
406    pub fn extract_forms(&self) -> Vec<FormInfo> {
407        self.dom
408            .as_ref()
409            .map(|d| d.extract_forms())
410            .unwrap_or_default()
411    }
412
413    pub fn extract_search_results(&self) -> Option<SearchResults> {
414        self.dom.as_ref().map(|d| d.parse_google_search_results())
415    }
416
417    pub async fn screenshot_async(&self) -> Option<crate::dom::ScreenshotResult> {
418        let dom = self.dom.as_ref()?;
419        let url = self.current_url.as_deref().unwrap_or("about:blank");
420        let results = dom.parse_google_search_results();
421        Some(
422            dom.screenshot_async(url, &results.page_title, self.current_url.as_deref())
423                .await,
424        )
425    }
426
427    pub fn screenshot(&self) -> Option<crate::dom::ScreenshotResult> {
428        let dom = self.dom.as_ref()?;
429        let url = self.current_url.as_deref().unwrap_or("about:blank");
430        let results = dom.parse_google_search_results();
431        Some(dom.screenshot(url, &results.page_title, self.current_url.as_deref()))
432    }
433
434    pub fn screenshot_svg(&self) -> Option<String> {
435        self.screenshot().map(|s| s.svg)
436    }
437
438    pub fn screenshot_layout(&self) -> Option<String> {
439        self.screenshot().map(|s| s.layout_wireframe)
440    }
441
442    pub async fn act_click(&mut self, target: &str) -> Result<Option<NavigationReport>> {
443        // Check if target is a numerical index
444        if let Ok(idx) = target.parse::<usize>() {
445            let elements = self.extract_interactive_elements();
446            if let Some(el) = elements.iter().find(|e| e.index == idx) {
447                if !el.href.is_empty() {
448                    let report = self.navigate(&el.href).await?;
449                    return Ok(Some(report));
450                }
451                return self.click(&el.selector).await;
452            }
453        }
454
455        self.click(target).await
456    }
457
458    pub async fn act_type(&mut self, target: &str, text: &str) -> Result<String> {
459        // Check if target is a numerical index
460        if let Ok(idx) = target.parse::<usize>() {
461            let elements = self.extract_interactive_elements();
462            if let Some(el) = elements.iter().find(|e| e.index == idx) {
463                return self.type_text(&el.selector, text);
464            }
465        }
466
467        self.type_text(target, text)
468    }
469
470    pub async fn click(&mut self, selector_or_text: &str) -> Result<Option<NavigationReport>> {
471        let links = self.extract_links();
472
473        // 1. Check if matches href or anchor text
474        if let Some(link) = links.iter().find(|l| {
475            l.text.eq_ignore_ascii_case(selector_or_text)
476                || l.href.contains(selector_or_text)
477                || l.text
478                    .to_lowercase()
479                    .contains(&selector_or_text.to_lowercase())
480        }) {
481            let report = self.navigate(&link.href).await?;
482            return Ok(Some(report));
483        }
484
485        // 2. Try evaluating JS click
486        let sel_json = serde_json::to_string(selector_or_text).unwrap_or_else(|_| format!("\"{}\"", selector_or_text));
487        let js_code = format!(
488            "var el = document.querySelector({}); if (el) {{ try {{ el.click(); }} catch(e){{}} true; }} else {{ false; }}",
489            sel_json
490        );
491        let _ = self.evaluate_js(&js_code);
492
493        Ok(None)
494    }
495
496    pub fn type_text(&mut self, selector: &str, text: &str) -> Result<String> {
497        let sel_json = serde_json::to_string(selector).unwrap_or_else(|_| format!("\"{}\"", selector));
498        let text_json = serde_json::to_string(text).unwrap_or_else(|_| format!("\"{}\"", text));
499        let js_code = format!(
500            r#"var el = document.querySelector({sel});
501            if (el) {{
502                if ('value' in el) {{
503                    el.value = {txt};
504                }} else {{
505                    el.innerText = {txt};
506                    el.textContent = {txt};
507                }}
508                try {{
509                    el.dispatchEvent(new Event('input', {{ bubbles: true }}));
510                    el.dispatchEvent(new Event('change', {{ bubbles: true }}));
511                }} catch(e) {{}}
512                'updated';
513            }} else {{
514                'not_found';
515            }}"#,
516            sel = sel_json,
517            txt = text_json
518        );
519        self.evaluate_js(&js_code)
520            .context("Failed to evaluate type action")
521    }
522}