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