adk_browser/
session.rs

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