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