1use 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#[derive(Debug, Clone)]
14pub struct ElementState {
15 pub is_displayed: bool,
17 pub is_enabled: bool,
19 pub is_selected: bool,
21 pub is_clickable: bool,
23}
24
25pub struct BrowserSession {
30 driver: RwLock<Option<WebDriver>>,
31 config: BrowserConfig,
32}
33
34impl BrowserSession {
35 pub fn new(config: BrowserConfig) -> Self {
40 Self { driver: RwLock::new(None), config }
41 }
42
43 pub fn with_defaults() -> Self {
45 Self::new(BrowserConfig::default())
46 }
47
48 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 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 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 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 pub async fn is_active(&self) -> bool {
114 let driver_guard = self.driver.read().await;
115 if let Some(ref driver) = *driver_guard {
116 driver.title().await.is_ok()
118 } else {
119 false
120 }
121 }
122
123 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 self.start().await
151 }
152
153 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 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 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 pub fn config(&self) -> &BrowserConfig {
193 &self.config
194 }
195
196 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 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 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 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 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 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 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 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 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 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 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 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 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 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 Ok(result.json().clone())
332 }
333
334 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 source
666 .js_drag_to(&target)
667 .await
668 .map_err(|e| AdkError::tool(format!("Drag and drop failed: {}", e)))
669 }
670
671 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 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 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 pub async fn press_key(
717 &self,
718 key: &str,
719 selector: Option<&str>,
720 modifiers: &[&str],
721 ) -> Result<()> {
722 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 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 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 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 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 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 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 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 tracing::debug!("BrowserSession dropped");
888 }
889}
890
891pub 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); }
905
906 #[tokio::test]
907 async fn test_session_not_started() {
908 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 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}