Skip to main content

adk_browser/
session.rs

1//! Browser session management wrapping thirtyfour WebDriver.
2
3use crate::config::{BrowserConfig, BrowserType};
4use crate::escape::escape_js_string;
5use adk_core::{AdkError, Result};
6use std::sync::Arc;
7use std::time::Duration;
8use thirtyfour::common::print::{PrintOrientation, PrintParameters};
9use thirtyfour::prelude::*;
10use tokio::sync::RwLock;
11
12/// State information about an element.
13#[derive(Debug, Clone)]
14pub struct ElementState {
15    /// Whether the element is displayed (visible).
16    pub is_displayed: bool,
17    /// Whether the element is enabled (not disabled).
18    pub is_enabled: bool,
19    /// Whether the element is selected (for checkboxes, radio buttons, options).
20    pub is_selected: bool,
21    /// Whether the element is clickable (displayed and enabled).
22    pub is_clickable: bool,
23}
24
25/// A browser session that wraps thirtyfour's WebDriver.
26///
27/// This is the core abstraction for browser automation in ADK.
28/// It can be shared across multiple tools via `Arc<BrowserSession>`.
29pub struct BrowserSession {
30    driver: RwLock<Option<WebDriver>>,
31    config: BrowserConfig,
32}
33
34impl BrowserSession {
35    /// Create a new browser session with the given configuration.
36    ///
37    /// Note: This does not start the browser immediately.
38    /// Call `start()` to initialize the WebDriver connection.
39    pub fn new(config: BrowserConfig) -> Self {
40        Self { driver: RwLock::new(None), config }
41    }
42
43    /// Create a browser session with default configuration.
44    pub fn with_defaults() -> Self {
45        Self::new(BrowserConfig::default())
46    }
47
48    /// Start the browser session by connecting to WebDriver.
49    pub async fn start(&self) -> Result<()> {
50        let mut driver_guard = self.driver.write().await;
51
52        if driver_guard.is_some() {
53            return Ok(()); // Already started
54        }
55
56        let caps = self.build_capabilities()?;
57        let driver = WebDriver::new(&self.config.webdriver_url, caps)
58            .await
59            .map_err(|e| AdkError::Tool(format!("Failed to start browser: {}", e)))?;
60
61        // Set timeouts
62        driver
63            .set_page_load_timeout(Duration::from_secs(self.config.page_load_timeout_secs))
64            .await
65            .map_err(|e| AdkError::Tool(format!("Failed to set page load timeout: {}", e)))?;
66
67        driver
68            .set_script_timeout(Duration::from_secs(self.config.script_timeout_secs))
69            .await
70            .map_err(|e| AdkError::Tool(format!("Failed to set script timeout: {}", e)))?;
71
72        driver
73            .set_implicit_wait_timeout(Duration::from_secs(self.config.implicit_wait_secs))
74            .await
75            .map_err(|e| AdkError::Tool(format!("Failed to set implicit wait: {}", e)))?;
76
77        // Set viewport size
78        driver
79            .set_window_rect(0, 0, self.config.viewport_width, self.config.viewport_height)
80            .await
81            .map_err(|e| AdkError::Tool(format!("Failed to set viewport: {}", e)))?;
82
83        *driver_guard = Some(driver);
84        Ok(())
85    }
86
87    /// Stop the browser session.
88    pub async fn stop(&self) -> Result<()> {
89        let mut driver_guard = self.driver.write().await;
90
91        if let Some(driver) = driver_guard.take() {
92            driver
93                .quit()
94                .await
95                .map_err(|e| AdkError::Tool(format!("Failed to quit browser: {}", e)))?;
96        }
97
98        Ok(())
99    }
100
101    /// Check if the session is active by verifying the WebDriver connection is alive.
102    pub async fn is_active(&self) -> bool {
103        let driver_guard = self.driver.read().await;
104        if let Some(ref driver) = *driver_guard {
105            // Actually ping the session to verify it's alive
106            driver.title().await.is_ok()
107        } else {
108            false
109        }
110    }
111
112    /// Ensure the browser session is started, reconnecting if the session died.
113    ///
114    /// Call this instead of checking `is_active()` manually. If the session
115    /// exists but is stale (WebDriver died), it will be recreated transparently.
116    pub async fn ensure_started(&self) -> Result<()> {
117        {
118            let driver_guard = self.driver.read().await;
119            if let Some(ref driver) = *driver_guard {
120                // Ping the session — if it responds, we're good
121                if driver.title().await.is_ok() {
122                    return Ok(());
123                }
124            }
125        }
126        // Session is dead or missing — (re)create it
127        // First clear the stale driver if any
128        {
129            let mut driver_guard = self.driver.write().await;
130            *driver_guard = None;
131        }
132        self.start().await
133    }
134
135    /// Internal: get a live WebDriver handle, auto-starting if needed.
136    /// All public methods that access the WebDriver MUST go through this.
137    ///
138    /// Returns a cloned WebDriver handle (cheap — WebDriver is Arc-based internally).
139    /// The RwLock is NOT held across the caller's WebDriver operations.
140    async fn live_driver(&self) -> Result<WebDriver> {
141        self.ensure_started().await?;
142
143        let guard = self.driver.read().await;
144        guard.clone().ok_or_else(|| {
145            AdkError::Tool(
146                "Failed to start browser session: driver is None after ensure_started()".into(),
147            )
148        })
149    }
150
151    /// Get a snapshot of the current page context (url, title, truncated body text).
152    ///
153    /// Useful for returning page state after interaction tools.
154    pub async fn page_context(&self) -> Result<serde_json::Value> {
155        let url = self.current_url().await.unwrap_or_default();
156        let title = self.title().await.unwrap_or_default();
157
158        // Get truncated visible text (first 2000 chars of body)
159        let page_text = self
160            .execute_script(
161                "return (document.body && document.body.innerText || '').substring(0, 2000);",
162            )
163            .await
164            .ok()
165            .and_then(|v| v.as_str().map(|s| s.to_string()))
166            .unwrap_or_default();
167
168        Ok(serde_json::json!({
169            "url": url,
170            "title": title,
171            "page_text": page_text
172        }))
173    }
174
175    /// Get the configuration.
176    pub fn config(&self) -> &BrowserConfig {
177        &self.config
178    }
179
180    /// Navigate to a URL.
181    pub async fn navigate(&self, url: &str) -> Result<()> {
182        let driver = self.live_driver().await?;
183
184        driver.goto(url).await.map_err(|e| AdkError::Tool(format!("Navigation failed: {}", e)))?;
185
186        Ok(())
187    }
188
189    /// Get the current URL.
190    pub async fn current_url(&self) -> Result<String> {
191        let driver = self.live_driver().await?;
192
193        driver
194            .current_url()
195            .await
196            .map(|u| u.to_string())
197            .map_err(|e| AdkError::Tool(format!("Failed to get URL: {}", e)))
198    }
199
200    /// Get the page title.
201    pub async fn title(&self) -> Result<String> {
202        let driver = self.live_driver().await?;
203
204        driver.title().await.map_err(|e| AdkError::Tool(format!("Failed to get title: {}", e)))
205    }
206
207    /// Find an element by CSS selector.
208    pub async fn find_element(&self, selector: &str) -> Result<WebElement> {
209        let driver = self.live_driver().await?;
210
211        driver
212            .find(By::Css(selector))
213            .await
214            .map_err(|e| AdkError::Tool(format!("Element not found '{}': {}", selector, e)))
215    }
216
217    /// Find multiple elements by CSS selector.
218    pub async fn find_elements(&self, selector: &str) -> Result<Vec<WebElement>> {
219        let driver = self.live_driver().await?;
220
221        driver
222            .find_all(By::Css(selector))
223            .await
224            .map_err(|e| AdkError::Tool(format!("Elements query failed '{}': {}", selector, e)))
225    }
226
227    /// Find element by XPath.
228    pub async fn find_by_xpath(&self, xpath: &str) -> Result<WebElement> {
229        let driver = self.live_driver().await?;
230
231        driver
232            .find(By::XPath(xpath))
233            .await
234            .map_err(|e| AdkError::Tool(format!("XPath not found '{}': {}", xpath, e)))
235    }
236
237    /// Click an element by selector.
238    pub async fn click(&self, selector: &str) -> Result<()> {
239        let element = self.find_element(selector).await?;
240        element
241            .click()
242            .await
243            .map_err(|e| AdkError::Tool(format!("Click failed on '{}': {}", selector, e)))
244    }
245
246    /// Type text into an element.
247    pub async fn type_text(&self, selector: &str, text: &str) -> Result<()> {
248        let element = self.find_element(selector).await?;
249        element
250            .send_keys(text)
251            .await
252            .map_err(|e| AdkError::Tool(format!("Type failed on '{}': {}", selector, e)))
253    }
254
255    /// Clear an input field.
256    pub async fn clear(&self, selector: &str) -> Result<()> {
257        let element = self.find_element(selector).await?;
258        element
259            .clear()
260            .await
261            .map_err(|e| AdkError::Tool(format!("Clear failed on '{}': {}", selector, e)))
262    }
263
264    /// Get text content of an element.
265    pub async fn get_text(&self, selector: &str) -> Result<String> {
266        let element = self.find_element(selector).await?;
267        element
268            .text()
269            .await
270            .map_err(|e| AdkError::Tool(format!("Get text failed on '{}': {}", selector, e)))
271    }
272
273    /// Get an attribute value.
274    pub async fn get_attribute(&self, selector: &str, attribute: &str) -> Result<Option<String>> {
275        let element = self.find_element(selector).await?;
276        element
277            .attr(attribute)
278            .await
279            .map_err(|e| AdkError::Tool(format!("Get attribute failed: {}", e)))
280    }
281
282    /// Take a screenshot (returns base64-encoded PNG).
283    pub async fn screenshot(&self) -> Result<String> {
284        let driver = self.live_driver().await?;
285
286        let screenshot = driver
287            .screenshot_as_png_base64()
288            .await
289            .map_err(|e| AdkError::Tool(format!("Screenshot failed: {}", e)))?;
290
291        Ok(screenshot)
292    }
293
294    /// Take a screenshot of a specific element.
295    pub async fn screenshot_element(&self, selector: &str) -> Result<String> {
296        let element = self.find_element(selector).await?;
297        let screenshot = element
298            .screenshot_as_png_base64()
299            .await
300            .map_err(|e| AdkError::Tool(format!("Element screenshot failed: {}", e)))?;
301
302        Ok(screenshot)
303    }
304
305    /// Execute JavaScript and return result.
306    pub async fn execute_script(&self, script: &str) -> Result<serde_json::Value> {
307        let driver = self.live_driver().await?;
308
309        let result = driver
310            .execute(script, vec![])
311            .await
312            .map_err(|e| AdkError::Tool(format!("Script execution failed: {}", e)))?;
313
314        // thirtyfour's ScriptRet provides .json() which returns &Value directly
315        Ok(result.json().clone())
316    }
317
318    /// Execute async JavaScript and return result.
319    pub async fn execute_async_script(&self, script: &str) -> Result<serde_json::Value> {
320        let driver = self.live_driver().await?;
321
322        let result = driver
323            .execute_async(script, vec![])
324            .await
325            .map_err(|e| AdkError::Tool(format!("Async script failed: {}", e)))?;
326
327        Ok(result.json().clone())
328    }
329
330    /// Wait for an element to be present.
331    pub async fn wait_for_element(&self, selector: &str, timeout_secs: u64) -> Result<WebElement> {
332        let driver = self.live_driver().await?;
333
334        driver
335            .query(By::Css(selector))
336            .wait(Duration::from_secs(timeout_secs), Duration::from_millis(100))
337            .first()
338            .await
339            .map_err(|e| {
340                AdkError::Tool(format!(
341                    "Timeout waiting for '{}' after {}s: {}",
342                    selector, timeout_secs, e
343                ))
344            })
345    }
346
347    /// Wait for an element to be clickable.
348    pub async fn wait_for_clickable(
349        &self,
350        selector: &str,
351        timeout_secs: u64,
352    ) -> Result<WebElement> {
353        let driver = self.live_driver().await?;
354
355        driver
356            .query(By::Css(selector))
357            .wait(Duration::from_secs(timeout_secs), Duration::from_millis(100))
358            .and_clickable()
359            .first()
360            .await
361            .map_err(|e| {
362                AdkError::Tool(format!("Timeout waiting for clickable '{}': {}", selector, e))
363            })
364    }
365
366    /// Get page source HTML.
367    pub async fn page_source(&self) -> Result<String> {
368        let driver = self.live_driver().await?;
369
370        driver
371            .source()
372            .await
373            .map_err(|e| AdkError::Tool(format!("Failed to get page source: {}", e)))
374    }
375
376    /// Go back in history.
377    pub async fn back(&self) -> Result<()> {
378        let driver = self.live_driver().await?;
379
380        driver.back().await.map_err(|e| AdkError::Tool(format!("Back navigation failed: {}", e)))
381    }
382
383    /// Go forward in history.
384    pub async fn forward(&self) -> Result<()> {
385        let driver = self.live_driver().await?;
386
387        driver
388            .forward()
389            .await
390            .map_err(|e| AdkError::Tool(format!("Forward navigation failed: {}", e)))
391    }
392
393    /// Refresh the current page.
394    pub async fn refresh(&self) -> Result<()> {
395        let driver = self.live_driver().await?;
396
397        driver.refresh().await.map_err(|e| AdkError::Tool(format!("Refresh failed: {}", e)))
398    }
399
400    // =========================================================================
401    // Cookie Management
402    // =========================================================================
403
404    /// Get all cookies.
405    pub async fn get_all_cookies(&self) -> Result<Vec<serde_json::Value>> {
406        let driver = self.live_driver().await?;
407
408        let cookies = driver
409            .get_all_cookies()
410            .await
411            .map_err(|e| AdkError::Tool(format!("Failed to get cookies: {}", e)))?;
412
413        Ok(cookies
414            .into_iter()
415            .map(|c| {
416                serde_json::json!({
417                    "name": c.name,
418                    "value": c.value,
419                    "domain": c.domain,
420                    "path": c.path,
421                    "secure": c.secure,
422                })
423            })
424            .collect())
425    }
426
427    /// Get a cookie by name.
428    pub async fn get_cookie(&self, name: &str) -> Result<serde_json::Value> {
429        let driver = self.live_driver().await?;
430
431        let cookie = driver
432            .get_named_cookie(name)
433            .await
434            .map_err(|e| AdkError::Tool(format!("Failed to get cookie '{}': {}", name, e)))?;
435
436        Ok(serde_json::json!({
437            "name": cookie.name,
438            "value": cookie.value,
439            "domain": cookie.domain,
440            "path": cookie.path,
441            "secure": cookie.secure,
442        }))
443    }
444
445    /// Add a cookie.
446    #[allow(clippy::too_many_arguments)]
447    pub async fn add_cookie(
448        &self,
449        name: &str,
450        value: &str,
451        domain: Option<&str>,
452        path: Option<&str>,
453        secure: Option<bool>,
454        expiry: Option<i64>,
455    ) -> Result<()> {
456        let driver = self.live_driver().await?;
457
458        let mut cookie = thirtyfour::Cookie::new(name, value);
459        if let Some(d) = domain {
460            cookie.set_domain(d);
461        }
462        if let Some(p) = path {
463            cookie.set_path(p);
464        }
465        if let Some(s) = secure {
466            cookie.set_secure(s);
467        }
468        if let Some(exp) = expiry {
469            // thirtyfour Cookie expiry expects seconds since epoch
470            cookie.set_expiry(exp);
471        }
472
473        driver
474            .add_cookie(cookie)
475            .await
476            .map_err(|e| AdkError::Tool(format!("Failed to add cookie: {e}")))
477    }
478
479    /// Delete a cookie.
480    pub async fn delete_cookie(&self, name: &str) -> Result<()> {
481        let driver = self.live_driver().await?;
482
483        driver
484            .delete_cookie(name)
485            .await
486            .map_err(|e| AdkError::Tool(format!("Failed to delete cookie: {}", e)))
487    }
488
489    /// Delete all cookies.
490    pub async fn delete_all_cookies(&self) -> Result<()> {
491        let driver = self.live_driver().await?;
492
493        driver
494            .delete_all_cookies()
495            .await
496            .map_err(|e| AdkError::Tool(format!("Failed to delete all cookies: {}", e)))
497    }
498
499    // =========================================================================
500    // Window Management
501    // =========================================================================
502
503    /// List all windows/tabs.
504    pub async fn list_windows(&self) -> Result<(Vec<String>, String)> {
505        let driver = self.live_driver().await?;
506
507        let windows = driver
508            .windows()
509            .await
510            .map_err(|e| AdkError::Tool(format!("Failed to get windows: {}", e)))?;
511
512        let current = driver
513            .window()
514            .await
515            .map_err(|e| AdkError::Tool(format!("Failed to get current window: {}", e)))?;
516
517        Ok((windows.into_iter().map(|w| w.to_string()).collect(), current.to_string()))
518    }
519
520    /// Open a new tab.
521    pub async fn new_tab(&self) -> Result<String> {
522        let driver = self.live_driver().await?;
523
524        let handle = driver
525            .new_tab()
526            .await
527            .map_err(|e| AdkError::Tool(format!("Failed to open new tab: {}", e)))?;
528
529        Ok(handle.to_string())
530    }
531
532    /// Open a new window.
533    pub async fn new_window(&self) -> Result<String> {
534        let driver = self.live_driver().await?;
535
536        let handle = driver
537            .new_window()
538            .await
539            .map_err(|e| AdkError::Tool(format!("Failed to open new window: {}", e)))?;
540
541        Ok(handle.to_string())
542    }
543
544    /// Switch to a window by handle.
545    pub async fn switch_to_window(&self, handle: &str) -> Result<()> {
546        let driver = self.live_driver().await?;
547
548        let window_handle = thirtyfour::WindowHandle::from(handle.to_string());
549        driver
550            .switch_to_window(window_handle)
551            .await
552            .map_err(|e| AdkError::Tool(format!("Failed to switch window: {}", e)))
553    }
554
555    /// Close the current window.
556    pub async fn close_window(&self) -> Result<()> {
557        let driver = self.live_driver().await?;
558
559        driver
560            .close_window()
561            .await
562            .map_err(|e| AdkError::Tool(format!("Failed to close window: {}", e)))
563    }
564
565    /// Maximize window.
566    pub async fn maximize_window(&self) -> Result<()> {
567        let driver = self.live_driver().await?;
568
569        driver
570            .maximize_window()
571            .await
572            .map_err(|e| AdkError::Tool(format!("Failed to maximize window: {}", e)))
573    }
574
575    /// Minimize window.
576    pub async fn minimize_window(&self) -> Result<()> {
577        let driver = self.live_driver().await?;
578
579        driver
580            .minimize_window()
581            .await
582            .map_err(|e| AdkError::Tool(format!("Failed to minimize window: {}", e)))
583    }
584
585    /// Set window size and position.
586    pub async fn set_window_rect(&self, x: i32, y: i32, width: u32, height: u32) -> Result<()> {
587        let driver = self.live_driver().await?;
588
589        driver
590            .set_window_rect(x as i64, y as i64, width, height)
591            .await
592            .map_err(|e| AdkError::Tool(format!("Failed to set window rect: {}", e)))
593    }
594
595    // =========================================================================
596    // Frame Management
597    // =========================================================================
598
599    /// Switch to frame by index.
600    pub async fn switch_to_frame_by_index(&self, index: u16) -> Result<()> {
601        let driver = self.live_driver().await?;
602
603        driver
604            .enter_frame(index)
605            .await
606            .map_err(|e| AdkError::Tool(format!("Failed to switch to frame {}: {}", index, e)))
607    }
608
609    /// Switch to frame by selector.
610    pub async fn switch_to_frame_by_selector(&self, selector: &str) -> Result<()> {
611        let element = self.find_element(selector).await?;
612
613        element
614            .enter_frame()
615            .await
616            .map_err(|e| AdkError::Tool(format!("Failed to switch to frame: {}", e)))
617    }
618
619    /// Switch to parent frame.
620    pub async fn switch_to_parent_frame(&self) -> Result<()> {
621        let driver = self.live_driver().await?;
622
623        driver
624            .enter_parent_frame()
625            .await
626            .map_err(|e| AdkError::Tool(format!("Failed to switch to parent frame: {}", e)))
627    }
628
629    /// Switch to default content.
630    pub async fn switch_to_default_content(&self) -> Result<()> {
631        let driver = self.live_driver().await?;
632
633        driver
634            .enter_default_frame()
635            .await
636            .map_err(|e| AdkError::Tool(format!("Failed to switch to default content: {}", e)))
637    }
638
639    // =========================================================================
640    // Advanced Actions
641    // =========================================================================
642
643    /// Drag and drop.
644    pub async fn drag_and_drop(&self, source_selector: &str, target_selector: &str) -> Result<()> {
645        let source = self.find_element(source_selector).await?;
646        let target = self.find_element(target_selector).await?;
647
648        // Use JavaScript-based drag and drop for broader compatibility
649        source
650            .js_drag_to(&target)
651            .await
652            .map_err(|e| AdkError::Tool(format!("Drag and drop failed: {}", e)))
653    }
654
655    /// Right-click (context click).
656    pub async fn right_click(&self, selector: &str) -> Result<()> {
657        let escaped = escape_js_string(selector);
658        let script = format!(
659            r#"
660            var element = document.querySelector('{escaped}');
661            if (element) {{
662                var event = new MouseEvent('contextmenu', {{
663                    'view': window,
664                    'bubbles': true,
665                    'cancelable': true
666                }});
667                element.dispatchEvent(event);
668                return true;
669            }}
670            return false;
671            "#,
672        );
673
674        let result = self.execute_script(&script).await?;
675        if result.as_bool() != Some(true) {
676            return Err(AdkError::Tool(format!("Element not found: {}", selector)));
677        }
678        Ok(())
679    }
680
681    /// Focus an element.
682    pub async fn focus_element(&self, selector: &str) -> Result<()> {
683        let element = self.find_element(selector).await?;
684        element.focus().await.map_err(|e| AdkError::Tool(format!("Focus failed: {}", e)))
685    }
686
687    /// Get element state.
688    pub async fn get_element_state(&self, selector: &str) -> Result<ElementState> {
689        let element = self.find_element(selector).await?;
690
691        let is_displayed = element.is_displayed().await.unwrap_or(false);
692        let is_enabled = element.is_enabled().await.unwrap_or(false);
693        let is_selected = element.is_selected().await.unwrap_or(false);
694        let is_clickable = element.is_clickable().await.unwrap_or(false);
695
696        Ok(ElementState { is_displayed, is_enabled, is_selected, is_clickable })
697    }
698
699    /// Press a key, optionally with modifier keys (Ctrl, Alt, Shift, Meta).
700    pub async fn press_key(
701        &self,
702        key: &str,
703        selector: Option<&str>,
704        modifiers: &[&str],
705    ) -> Result<()> {
706        // Map modifier names to WebDriver key codes
707        let modifier_codes: Vec<&str> = modifiers
708            .iter()
709            .filter_map(|m| match m.to_lowercase().as_str() {
710                "ctrl" | "control" => Some("\u{E009}"),
711                "alt" => Some("\u{E00A}"),
712                "shift" => Some("\u{E008}"),
713                "meta" | "command" | "cmd" => Some("\u{E03D}"),
714                _ => None,
715            })
716            .collect();
717
718        let key_str = match key.to_lowercase().as_str() {
719            "enter" => "\u{E007}",
720            "tab" => "\u{E004}",
721            "escape" | "esc" => "\u{E00C}",
722            "backspace" => "\u{E003}",
723            "delete" => "\u{E017}",
724            "arrowup" | "up" => "\u{E013}",
725            "arrowdown" | "down" => "\u{E015}",
726            "arrowleft" | "left" => "\u{E012}",
727            "arrowright" | "right" => "\u{E014}",
728            "home" => "\u{E011}",
729            "end" => "\u{E010}",
730            "pageup" => "\u{E00E}",
731            "pagedown" => "\u{E00F}",
732            "space" => " ",
733            _ => key,
734        };
735
736        // Build the full key sequence: modifiers down + key + modifiers up
737        let mut key_sequence = String::new();
738        for code in &modifier_codes {
739            key_sequence.push_str(code);
740        }
741        key_sequence.push_str(key_str);
742        // Release modifiers in reverse order
743        for code in modifier_codes.iter().rev() {
744            key_sequence.push_str(code);
745        }
746
747        if let Some(sel) = selector {
748            let element = self.find_element(sel).await?;
749            element
750                .send_keys(&key_sequence)
751                .await
752                .map_err(|e| AdkError::Tool(format!("Key press failed: {e}")))?;
753        } else {
754            // Send to active element via WebDriver
755            let driver = self.live_driver().await?;
756
757            let active = driver
758                .active_element()
759                .await
760                .map_err(|e| AdkError::Tool(format!("No active element: {e}")))?;
761            active
762                .send_keys(&key_sequence)
763                .await
764                .map_err(|e| AdkError::Tool(format!("Key press failed: {e}")))?;
765        }
766
767        Ok(())
768    }
769
770    /// Upload a file.
771    pub async fn upload_file(&self, selector: &str, file_path: &str) -> Result<()> {
772        let element = self.find_element(selector).await?;
773        element
774            .send_keys(file_path)
775            .await
776            .map_err(|e| AdkError::Tool(format!("File upload failed: {}", e)))
777    }
778
779    /// Print page to PDF.
780    pub async fn print_to_pdf(&self, landscape: bool, scale: f64) -> Result<String> {
781        let driver = self.live_driver().await?;
782
783        let params = PrintParameters {
784            orientation: if landscape {
785                PrintOrientation::Landscape
786            } else {
787                PrintOrientation::Portrait
788            },
789            scale,
790            ..Default::default()
791        };
792
793        driver
794            .print_page_base64(params)
795            .await
796            .map_err(|e| AdkError::Tool(format!("Print to PDF failed: {}", e)))
797    }
798
799    /// Build browser capabilities based on configuration.
800    fn build_capabilities(&self) -> Result<Capabilities> {
801        let caps = match self.config.browser {
802            BrowserType::Chrome => {
803                let mut caps = DesiredCapabilities::chrome();
804                if self.config.headless {
805                    caps.add_arg("--headless=new").map_err(|e| {
806                        AdkError::Tool(format!("Failed to add headless arg: {}", e))
807                    })?;
808                }
809                caps.add_arg("--no-sandbox")
810                    .map_err(|e| AdkError::Tool(format!("Failed to add no-sandbox: {}", e)))?;
811                caps.add_arg("--disable-dev-shm-usage")
812                    .map_err(|e| AdkError::Tool(format!("Failed to add disable-dev-shm: {}", e)))?;
813
814                if let Some(ref ua) = self.config.user_agent {
815                    caps.add_arg(&format!("--user-agent={}", ua))
816                        .map_err(|e| AdkError::Tool(format!("Failed to add user-agent: {}", e)))?;
817                }
818
819                for arg in &self.config.browser_args {
820                    caps.add_arg(arg).map_err(|e| {
821                        AdkError::Tool(format!("Failed to add arg '{}': {}", arg, e))
822                    })?;
823                }
824
825                caps.into()
826            }
827            BrowserType::Firefox => {
828                let mut caps = DesiredCapabilities::firefox();
829                if self.config.headless {
830                    caps.add_arg("-headless")
831                        .map_err(|e| AdkError::Tool(format!("Failed to add headless: {}", e)))?;
832                }
833                caps.into()
834            }
835            BrowserType::Safari => DesiredCapabilities::safari().into(),
836            BrowserType::Edge => {
837                let mut caps = DesiredCapabilities::edge();
838                if self.config.headless {
839                    caps.add_arg("--headless")
840                        .map_err(|e| AdkError::Tool(format!("Failed to add headless: {}", e)))?;
841                }
842                caps.into()
843            }
844        };
845
846        Ok(caps)
847    }
848}
849
850impl Drop for BrowserSession {
851    fn drop(&mut self) {
852        // Note: Can't do async cleanup in Drop, but thirtyfour handles this gracefully
853        tracing::debug!("BrowserSession dropped");
854    }
855}
856
857/// Create a shared browser session.
858pub fn shared_session(config: BrowserConfig) -> Arc<BrowserSession> {
859    Arc::new(BrowserSession::new(config))
860}
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865
866    #[test]
867    fn test_session_creation() {
868        let session = BrowserSession::with_defaults();
869        assert!(!session.config().headless || session.config().headless); // Always true, just testing creation
870    }
871
872    #[tokio::test]
873    async fn test_session_not_started() {
874        // Use a bogus WebDriver URL so ensure_started's auto-start attempt fails
875        let config = BrowserConfig::new().webdriver_url("http://127.0.0.1:1");
876        let session = BrowserSession::new(config);
877        assert!(!session.is_active().await);
878
879        // navigate() calls live_driver() → ensure_started() → start(),
880        // which fails because the WebDriver is unreachable.
881        let result = session.navigate("https://example.com").await;
882        assert!(result.is_err());
883    }
884
885    #[test]
886    fn test_build_capabilities_chrome_headless() {
887        let config = BrowserConfig::new().headless(true);
888        let session = BrowserSession::new(config);
889        let caps = session.build_capabilities();
890        assert!(caps.is_ok());
891    }
892
893    #[test]
894    fn test_build_capabilities_firefox() {
895        let config = BrowserConfig::new().browser(BrowserType::Firefox);
896        let session = BrowserSession::new(config);
897        let caps = session.build_capabilities();
898        assert!(caps.is_ok());
899    }
900
901    #[test]
902    fn test_build_capabilities_safari() {
903        let config = BrowserConfig::new().browser(BrowserType::Safari);
904        let session = BrowserSession::new(config);
905        let caps = session.build_capabilities();
906        assert!(caps.is_ok());
907    }
908
909    #[test]
910    fn test_build_capabilities_edge() {
911        let config = BrowserConfig::new().browser(BrowserType::Edge);
912        let session = BrowserSession::new(config);
913        let caps = session.build_capabilities();
914        assert!(caps.is_ok());
915    }
916
917    #[test]
918    fn test_build_capabilities_with_user_agent() {
919        let config = BrowserConfig::new().user_agent("CustomAgent/1.0");
920        let session = BrowserSession::new(config);
921        let caps = session.build_capabilities();
922        assert!(caps.is_ok());
923    }
924
925    #[test]
926    fn test_build_capabilities_with_extra_args() {
927        let config = BrowserConfig::new().add_arg("--disable-gpu").add_arg("--window-size=800,600");
928        let session = BrowserSession::new(config);
929        let caps = session.build_capabilities();
930        assert!(caps.is_ok());
931    }
932}