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<()> {
50 let mut driver_guard = self.driver.write().await;
51
52 if driver_guard.is_some() {
53 return Ok(()); }
55
56 let caps = self.build_capabilities()?;
57 let driver = WebDriver::new(&self.config.webdriver_url, caps)
58 .await
59 .map_err(|e| AdkError::tool(format!("Failed to start browser: {}", e)))?;
60
61 driver
63 .set_page_load_timeout(Duration::from_secs(self.config.page_load_timeout_secs))
64 .await
65 .map_err(|e| AdkError::tool(format!("Failed to set page load timeout: {}", e)))?;
66
67 driver
68 .set_script_timeout(Duration::from_secs(self.config.script_timeout_secs))
69 .await
70 .map_err(|e| AdkError::tool(format!("Failed to set script timeout: {}", e)))?;
71
72 driver
73 .set_implicit_wait_timeout(Duration::from_secs(self.config.implicit_wait_secs))
74 .await
75 .map_err(|e| AdkError::tool(format!("Failed to set implicit wait: {}", e)))?;
76
77 driver
79 .set_window_rect(0, 0, self.config.viewport_width, self.config.viewport_height)
80 .await
81 .map_err(|e| AdkError::tool(format!("Failed to set viewport: {}", e)))?;
82
83 *driver_guard = Some(driver);
84 Ok(())
85 }
86
87 pub async fn stop(&self) -> Result<()> {
89 let mut driver_guard = self.driver.write().await;
90
91 if let Some(driver) = driver_guard.take() {
92 driver
93 .quit()
94 .await
95 .map_err(|e| AdkError::tool(format!("Failed to quit browser: {}", e)))?;
96 }
97
98 Ok(())
99 }
100
101 pub async fn is_active(&self) -> bool {
103 let driver_guard = self.driver.read().await;
104 if let Some(ref driver) = *driver_guard {
105 driver.title().await.is_ok()
107 } else {
108 false
109 }
110 }
111
112 pub async fn ensure_started(&self) -> Result<()> {
117 {
118 let driver_guard = self.driver.read().await;
119 if let Some(ref driver) = *driver_guard {
120 if driver.title().await.is_ok() {
122 return Ok(());
123 }
124 }
125 }
126 {
129 let mut driver_guard = self.driver.write().await;
130 *driver_guard = None;
131 }
132 self.start().await
133 }
134
135 async fn live_driver(&self) -> Result<WebDriver> {
141 self.ensure_started().await?;
142
143 let guard = self.driver.read().await;
144 guard.clone().ok_or_else(|| {
145 AdkError::tool("Failed to start browser session: driver is None after ensure_started()")
146 })
147 }
148
149 pub async fn page_context(&self) -> Result<serde_json::Value> {
153 let url = self.current_url().await.unwrap_or_default();
154 let title = self.title().await.unwrap_or_default();
155
156 let page_text = self
158 .execute_script(
159 "return (document.body && document.body.innerText || '').substring(0, 2000);",
160 )
161 .await
162 .ok()
163 .and_then(|v| v.as_str().map(|s| s.to_string()))
164 .unwrap_or_default();
165
166 Ok(serde_json::json!({
167 "url": url,
168 "title": title,
169 "page_text": page_text
170 }))
171 }
172
173 pub fn config(&self) -> &BrowserConfig {
175 &self.config
176 }
177
178 pub async fn navigate(&self, url: &str) -> Result<()> {
180 let driver = self.live_driver().await?;
181
182 driver.goto(url).await.map_err(|e| AdkError::tool(format!("Navigation failed: {}", e)))?;
183
184 Ok(())
185 }
186
187 pub async fn current_url(&self) -> Result<String> {
189 let driver = self.live_driver().await?;
190
191 driver
192 .current_url()
193 .await
194 .map(|u| u.to_string())
195 .map_err(|e| AdkError::tool(format!("Failed to get URL: {}", e)))
196 }
197
198 pub async fn title(&self) -> Result<String> {
200 let driver = self.live_driver().await?;
201
202 driver.title().await.map_err(|e| AdkError::tool(format!("Failed to get title: {}", e)))
203 }
204
205 pub async fn find_element(&self, selector: &str) -> Result<WebElement> {
207 let driver = self.live_driver().await?;
208
209 driver
210 .find(By::Css(selector))
211 .await
212 .map_err(|e| AdkError::tool(format!("Element not found '{}': {}", selector, e)))
213 }
214
215 pub async fn find_elements(&self, selector: &str) -> Result<Vec<WebElement>> {
217 let driver = self.live_driver().await?;
218
219 driver
220 .find_all(By::Css(selector))
221 .await
222 .map_err(|e| AdkError::tool(format!("Elements query failed '{}': {}", selector, e)))
223 }
224
225 pub async fn find_by_xpath(&self, xpath: &str) -> Result<WebElement> {
227 let driver = self.live_driver().await?;
228
229 driver
230 .find(By::XPath(xpath))
231 .await
232 .map_err(|e| AdkError::tool(format!("XPath not found '{}': {}", xpath, e)))
233 }
234
235 pub async fn click(&self, selector: &str) -> Result<()> {
237 let element = self.find_element(selector).await?;
238 element
239 .click()
240 .await
241 .map_err(|e| AdkError::tool(format!("Click failed on '{}': {}", selector, e)))
242 }
243
244 pub async fn type_text(&self, selector: &str, text: &str) -> Result<()> {
246 let element = self.find_element(selector).await?;
247 element
248 .send_keys(text)
249 .await
250 .map_err(|e| AdkError::tool(format!("Type failed on '{}': {}", selector, e)))
251 }
252
253 pub async fn clear(&self, selector: &str) -> Result<()> {
255 let element = self.find_element(selector).await?;
256 element
257 .clear()
258 .await
259 .map_err(|e| AdkError::tool(format!("Clear failed on '{}': {}", selector, e)))
260 }
261
262 pub async fn get_text(&self, selector: &str) -> Result<String> {
264 let element = self.find_element(selector).await?;
265 element
266 .text()
267 .await
268 .map_err(|e| AdkError::tool(format!("Get text failed on '{}': {}", selector, e)))
269 }
270
271 pub async fn get_attribute(&self, selector: &str, attribute: &str) -> Result<Option<String>> {
273 let element = self.find_element(selector).await?;
274 element
275 .attr(attribute)
276 .await
277 .map_err(|e| AdkError::tool(format!("Get attribute failed: {}", e)))
278 }
279
280 pub async fn screenshot(&self) -> Result<String> {
282 let driver = self.live_driver().await?;
283
284 let screenshot = driver
285 .screenshot_as_png_base64()
286 .await
287 .map_err(|e| AdkError::tool(format!("Screenshot failed: {}", e)))?;
288
289 Ok(screenshot)
290 }
291
292 pub async fn screenshot_element(&self, selector: &str) -> Result<String> {
294 let element = self.find_element(selector).await?;
295 let screenshot = element
296 .screenshot_as_png_base64()
297 .await
298 .map_err(|e| AdkError::tool(format!("Element screenshot failed: {}", e)))?;
299
300 Ok(screenshot)
301 }
302
303 pub async fn execute_script(&self, script: &str) -> Result<serde_json::Value> {
305 let driver = self.live_driver().await?;
306
307 let result = driver
308 .execute(script, vec![])
309 .await
310 .map_err(|e| AdkError::tool(format!("Script execution failed: {}", e)))?;
311
312 Ok(result.json().clone())
314 }
315
316 pub async fn execute_async_script(&self, script: &str) -> Result<serde_json::Value> {
318 let driver = self.live_driver().await?;
319
320 let result = driver
321 .execute_async(script, vec![])
322 .await
323 .map_err(|e| AdkError::tool(format!("Async script failed: {}", e)))?;
324
325 Ok(result.json().clone())
326 }
327
328 pub async fn wait_for_element(&self, selector: &str, timeout_secs: u64) -> Result<WebElement> {
330 let driver = self.live_driver().await?;
331
332 driver
333 .query(By::Css(selector))
334 .wait(Duration::from_secs(timeout_secs), Duration::from_millis(100))
335 .first()
336 .await
337 .map_err(|e| {
338 AdkError::tool(format!(
339 "Timeout waiting for '{}' after {}s: {}",
340 selector, timeout_secs, e
341 ))
342 })
343 }
344
345 pub async fn wait_for_clickable(
347 &self,
348 selector: &str,
349 timeout_secs: u64,
350 ) -> Result<WebElement> {
351 let driver = self.live_driver().await?;
352
353 driver
354 .query(By::Css(selector))
355 .wait(Duration::from_secs(timeout_secs), Duration::from_millis(100))
356 .and_clickable()
357 .first()
358 .await
359 .map_err(|e| {
360 AdkError::tool(format!("Timeout waiting for clickable '{}': {}", selector, e))
361 })
362 }
363
364 pub async fn page_source(&self) -> Result<String> {
366 let driver = self.live_driver().await?;
367
368 driver
369 .source()
370 .await
371 .map_err(|e| AdkError::tool(format!("Failed to get page source: {}", e)))
372 }
373
374 pub async fn back(&self) -> Result<()> {
376 let driver = self.live_driver().await?;
377
378 driver.back().await.map_err(|e| AdkError::tool(format!("Back navigation failed: {}", e)))
379 }
380
381 pub async fn forward(&self) -> Result<()> {
383 let driver = self.live_driver().await?;
384
385 driver
386 .forward()
387 .await
388 .map_err(|e| AdkError::tool(format!("Forward navigation failed: {}", e)))
389 }
390
391 pub async fn refresh(&self) -> Result<()> {
393 let driver = self.live_driver().await?;
394
395 driver.refresh().await.map_err(|e| AdkError::tool(format!("Refresh failed: {}", e)))
396 }
397
398 pub async fn get_all_cookies(&self) -> Result<Vec<serde_json::Value>> {
404 let driver = self.live_driver().await?;
405
406 let cookies = driver
407 .get_all_cookies()
408 .await
409 .map_err(|e| AdkError::tool(format!("Failed to get cookies: {}", e)))?;
410
411 Ok(cookies
412 .into_iter()
413 .map(|c| {
414 serde_json::json!({
415 "name": c.name,
416 "value": c.value,
417 "domain": c.domain,
418 "path": c.path,
419 "secure": c.secure,
420 })
421 })
422 .collect())
423 }
424
425 pub async fn get_cookie(&self, name: &str) -> Result<serde_json::Value> {
427 let driver = self.live_driver().await?;
428
429 let cookie = driver
430 .get_named_cookie(name)
431 .await
432 .map_err(|e| AdkError::tool(format!("Failed to get cookie '{}': {}", name, e)))?;
433
434 Ok(serde_json::json!({
435 "name": cookie.name,
436 "value": cookie.value,
437 "domain": cookie.domain,
438 "path": cookie.path,
439 "secure": cookie.secure,
440 }))
441 }
442
443 #[allow(clippy::too_many_arguments)]
445 pub async fn add_cookie(
446 &self,
447 name: &str,
448 value: &str,
449 domain: Option<&str>,
450 path: Option<&str>,
451 secure: Option<bool>,
452 expiry: Option<i64>,
453 ) -> Result<()> {
454 let driver = self.live_driver().await?;
455
456 let mut cookie = thirtyfour::Cookie::new(name, value);
457 if let Some(d) = domain {
458 cookie.set_domain(d);
459 }
460 if let Some(p) = path {
461 cookie.set_path(p);
462 }
463 if let Some(s) = secure {
464 cookie.set_secure(s);
465 }
466 if let Some(exp) = expiry {
467 cookie.set_expiry(exp);
469 }
470
471 driver
472 .add_cookie(cookie)
473 .await
474 .map_err(|e| AdkError::tool(format!("Failed to add cookie: {e}")))
475 }
476
477 pub async fn delete_cookie(&self, name: &str) -> Result<()> {
479 let driver = self.live_driver().await?;
480
481 driver
482 .delete_cookie(name)
483 .await
484 .map_err(|e| AdkError::tool(format!("Failed to delete cookie: {}", e)))
485 }
486
487 pub async fn delete_all_cookies(&self) -> Result<()> {
489 let driver = self.live_driver().await?;
490
491 driver
492 .delete_all_cookies()
493 .await
494 .map_err(|e| AdkError::tool(format!("Failed to delete all cookies: {}", e)))
495 }
496
497 pub async fn list_windows(&self) -> Result<(Vec<String>, String)> {
503 let driver = self.live_driver().await?;
504
505 let windows = driver
506 .windows()
507 .await
508 .map_err(|e| AdkError::tool(format!("Failed to get windows: {}", e)))?;
509
510 let current = driver
511 .window()
512 .await
513 .map_err(|e| AdkError::tool(format!("Failed to get current window: {}", e)))?;
514
515 Ok((windows.into_iter().map(|w| w.to_string()).collect(), current.to_string()))
516 }
517
518 pub async fn new_tab(&self) -> Result<String> {
520 let driver = self.live_driver().await?;
521
522 let handle = driver
523 .new_tab()
524 .await
525 .map_err(|e| AdkError::tool(format!("Failed to open new tab: {}", e)))?;
526
527 Ok(handle.to_string())
528 }
529
530 pub async fn new_window(&self) -> Result<String> {
532 let driver = self.live_driver().await?;
533
534 let handle = driver
535 .new_window()
536 .await
537 .map_err(|e| AdkError::tool(format!("Failed to open new window: {}", e)))?;
538
539 Ok(handle.to_string())
540 }
541
542 pub async fn switch_to_window(&self, handle: &str) -> Result<()> {
544 let driver = self.live_driver().await?;
545
546 let window_handle = thirtyfour::WindowHandle::from(handle.to_string());
547 driver
548 .switch_to_window(window_handle)
549 .await
550 .map_err(|e| AdkError::tool(format!("Failed to switch window: {}", e)))
551 }
552
553 pub async fn close_window(&self) -> Result<()> {
555 let driver = self.live_driver().await?;
556
557 driver
558 .close_window()
559 .await
560 .map_err(|e| AdkError::tool(format!("Failed to close window: {}", e)))
561 }
562
563 pub async fn maximize_window(&self) -> Result<()> {
565 let driver = self.live_driver().await?;
566
567 driver
568 .maximize_window()
569 .await
570 .map_err(|e| AdkError::tool(format!("Failed to maximize window: {}", e)))
571 }
572
573 pub async fn minimize_window(&self) -> Result<()> {
575 let driver = self.live_driver().await?;
576
577 driver
578 .minimize_window()
579 .await
580 .map_err(|e| AdkError::tool(format!("Failed to minimize window: {}", e)))
581 }
582
583 pub async fn set_window_rect(&self, x: i32, y: i32, width: u32, height: u32) -> Result<()> {
585 let driver = self.live_driver().await?;
586
587 driver
588 .set_window_rect(x as i64, y as i64, width, height)
589 .await
590 .map_err(|e| AdkError::tool(format!("Failed to set window rect: {}", e)))
591 }
592
593 pub async fn switch_to_frame_by_index(&self, index: u16) -> Result<()> {
599 let driver = self.live_driver().await?;
600
601 driver
602 .enter_frame(index)
603 .await
604 .map_err(|e| AdkError::tool(format!("Failed to switch to frame {}: {}", index, e)))
605 }
606
607 pub async fn switch_to_frame_by_selector(&self, selector: &str) -> Result<()> {
609 let element = self.find_element(selector).await?;
610
611 element
612 .enter_frame()
613 .await
614 .map_err(|e| AdkError::tool(format!("Failed to switch to frame: {}", e)))
615 }
616
617 pub async fn switch_to_parent_frame(&self) -> Result<()> {
619 let driver = self.live_driver().await?;
620
621 driver
622 .enter_parent_frame()
623 .await
624 .map_err(|e| AdkError::tool(format!("Failed to switch to parent frame: {}", e)))
625 }
626
627 pub async fn switch_to_default_content(&self) -> Result<()> {
629 let driver = self.live_driver().await?;
630
631 driver
632 .enter_default_frame()
633 .await
634 .map_err(|e| AdkError::tool(format!("Failed to switch to default content: {}", e)))
635 }
636
637 pub async fn drag_and_drop(&self, source_selector: &str, target_selector: &str) -> Result<()> {
643 let source = self.find_element(source_selector).await?;
644 let target = self.find_element(target_selector).await?;
645
646 source
648 .js_drag_to(&target)
649 .await
650 .map_err(|e| AdkError::tool(format!("Drag and drop failed: {}", e)))
651 }
652
653 pub async fn right_click(&self, selector: &str) -> Result<()> {
655 let escaped = escape_js_string(selector);
656 let script = format!(
657 r#"
658 var element = document.querySelector('{escaped}');
659 if (element) {{
660 var event = new MouseEvent('contextmenu', {{
661 'view': window,
662 'bubbles': true,
663 'cancelable': true
664 }});
665 element.dispatchEvent(event);
666 return true;
667 }}
668 return false;
669 "#,
670 );
671
672 let result = self.execute_script(&script).await?;
673 if result.as_bool() != Some(true) {
674 return Err(AdkError::tool(format!("Element not found: {}", selector)));
675 }
676 Ok(())
677 }
678
679 pub async fn focus_element(&self, selector: &str) -> Result<()> {
681 let element = self.find_element(selector).await?;
682 element.focus().await.map_err(|e| AdkError::tool(format!("Focus failed: {}", e)))
683 }
684
685 pub async fn get_element_state(&self, selector: &str) -> Result<ElementState> {
687 let element = self.find_element(selector).await?;
688
689 let is_displayed = element.is_displayed().await.unwrap_or(false);
690 let is_enabled = element.is_enabled().await.unwrap_or(false);
691 let is_selected = element.is_selected().await.unwrap_or(false);
692 let is_clickable = element.is_clickable().await.unwrap_or(false);
693
694 Ok(ElementState { is_displayed, is_enabled, is_selected, is_clickable })
695 }
696
697 pub async fn press_key(
699 &self,
700 key: &str,
701 selector: Option<&str>,
702 modifiers: &[&str],
703 ) -> Result<()> {
704 let modifier_codes: Vec<&str> = modifiers
706 .iter()
707 .filter_map(|m| match m.to_lowercase().as_str() {
708 "ctrl" | "control" => Some("\u{E009}"),
709 "alt" => Some("\u{E00A}"),
710 "shift" => Some("\u{E008}"),
711 "meta" | "command" | "cmd" => Some("\u{E03D}"),
712 _ => None,
713 })
714 .collect();
715
716 let key_str = match key.to_lowercase().as_str() {
717 "enter" => "\u{E007}",
718 "tab" => "\u{E004}",
719 "escape" | "esc" => "\u{E00C}",
720 "backspace" => "\u{E003}",
721 "delete" => "\u{E017}",
722 "arrowup" | "up" => "\u{E013}",
723 "arrowdown" | "down" => "\u{E015}",
724 "arrowleft" | "left" => "\u{E012}",
725 "arrowright" | "right" => "\u{E014}",
726 "home" => "\u{E011}",
727 "end" => "\u{E010}",
728 "pageup" => "\u{E00E}",
729 "pagedown" => "\u{E00F}",
730 "space" => " ",
731 _ => key,
732 };
733
734 let mut key_sequence = String::new();
736 for code in &modifier_codes {
737 key_sequence.push_str(code);
738 }
739 key_sequence.push_str(key_str);
740 for code in modifier_codes.iter().rev() {
742 key_sequence.push_str(code);
743 }
744
745 if let Some(sel) = selector {
746 let element = self.find_element(sel).await?;
747 element
748 .send_keys(&key_sequence)
749 .await
750 .map_err(|e| AdkError::tool(format!("Key press failed: {e}")))?;
751 } else {
752 let driver = self.live_driver().await?;
754
755 let active = driver
756 .active_element()
757 .await
758 .map_err(|e| AdkError::tool(format!("No active element: {e}")))?;
759 active
760 .send_keys(&key_sequence)
761 .await
762 .map_err(|e| AdkError::tool(format!("Key press failed: {e}")))?;
763 }
764
765 Ok(())
766 }
767
768 pub async fn upload_file(&self, selector: &str, file_path: &str) -> Result<()> {
770 let element = self.find_element(selector).await?;
771 element
772 .send_keys(file_path)
773 .await
774 .map_err(|e| AdkError::tool(format!("File upload failed: {}", e)))
775 }
776
777 pub async fn print_to_pdf(&self, landscape: bool, scale: f64) -> Result<String> {
779 let driver = self.live_driver().await?;
780
781 let params = PrintParameters {
782 orientation: if landscape {
783 PrintOrientation::Landscape
784 } else {
785 PrintOrientation::Portrait
786 },
787 scale,
788 ..Default::default()
789 };
790
791 driver
792 .print_page_base64(params)
793 .await
794 .map_err(|e| AdkError::tool(format!("Print to PDF failed: {}", e)))
795 }
796
797 fn build_capabilities(&self) -> Result<Capabilities> {
799 let caps = match self.config.browser {
800 BrowserType::Chrome => {
801 let mut caps = DesiredCapabilities::chrome();
802 if self.config.headless {
803 caps.add_arg("--headless=new").map_err(|e| {
804 AdkError::tool(format!("Failed to add headless arg: {}", e))
805 })?;
806 }
807 caps.add_arg("--no-sandbox")
808 .map_err(|e| AdkError::tool(format!("Failed to add no-sandbox: {}", e)))?;
809 caps.add_arg("--disable-dev-shm-usage")
810 .map_err(|e| AdkError::tool(format!("Failed to add disable-dev-shm: {}", e)))?;
811
812 if let Some(ref ua) = self.config.user_agent {
813 caps.add_arg(&format!("--user-agent={}", ua))
814 .map_err(|e| AdkError::tool(format!("Failed to add user-agent: {}", e)))?;
815 }
816
817 for arg in &self.config.browser_args {
818 caps.add_arg(arg).map_err(|e| {
819 AdkError::tool(format!("Failed to add arg '{}': {}", arg, e))
820 })?;
821 }
822
823 caps.into()
824 }
825 BrowserType::Firefox => {
826 let mut caps = DesiredCapabilities::firefox();
827 if self.config.headless {
828 caps.add_arg("-headless")
829 .map_err(|e| AdkError::tool(format!("Failed to add headless: {}", e)))?;
830 }
831 caps.into()
832 }
833 BrowserType::Safari => DesiredCapabilities::safari().into(),
834 BrowserType::Edge => {
835 let mut caps = DesiredCapabilities::edge();
836 if self.config.headless {
837 caps.add_arg("--headless")
838 .map_err(|e| AdkError::tool(format!("Failed to add headless: {}", e)))?;
839 }
840 caps.into()
841 }
842 };
843
844 Ok(caps)
845 }
846}
847
848impl Drop for BrowserSession {
849 fn drop(&mut self) {
850 tracing::debug!("BrowserSession dropped");
852 }
853}
854
855pub fn shared_session(config: BrowserConfig) -> Arc<BrowserSession> {
857 Arc::new(BrowserSession::new(config))
858}
859
860#[cfg(test)]
861mod tests {
862 use super::*;
863
864 #[test]
865 fn test_session_creation() {
866 let session = BrowserSession::with_defaults();
867 assert!(!session.config().headless || session.config().headless); }
869
870 #[tokio::test]
871 async fn test_session_not_started() {
872 let config = BrowserConfig::new().webdriver_url("http://127.0.0.1:1");
874 let session = BrowserSession::new(config);
875 assert!(!session.is_active().await);
876
877 let result = session.navigate("https://example.com").await;
880 assert!(result.is_err());
881 }
882
883 #[test]
884 fn test_build_capabilities_chrome_headless() {
885 let config = BrowserConfig::new().headless(true);
886 let session = BrowserSession::new(config);
887 let caps = session.build_capabilities();
888 assert!(caps.is_ok());
889 }
890
891 #[test]
892 fn test_build_capabilities_firefox() {
893 let config = BrowserConfig::new().browser(BrowserType::Firefox);
894 let session = BrowserSession::new(config);
895 let caps = session.build_capabilities();
896 assert!(caps.is_ok());
897 }
898
899 #[test]
900 fn test_build_capabilities_safari() {
901 let config = BrowserConfig::new().browser(BrowserType::Safari);
902 let session = BrowserSession::new(config);
903 let caps = session.build_capabilities();
904 assert!(caps.is_ok());
905 }
906
907 #[test]
908 fn test_build_capabilities_edge() {
909 let config = BrowserConfig::new().browser(BrowserType::Edge);
910 let session = BrowserSession::new(config);
911 let caps = session.build_capabilities();
912 assert!(caps.is_ok());
913 }
914
915 #[test]
916 fn test_build_capabilities_with_user_agent() {
917 let config = BrowserConfig::new().user_agent("CustomAgent/1.0");
918 let session = BrowserSession::new(config);
919 let caps = session.build_capabilities();
920 assert!(caps.is_ok());
921 }
922
923 #[test]
924 fn test_build_capabilities_with_extra_args() {
925 let config = BrowserConfig::new().add_arg("--disable-gpu").add_arg("--window-size=800,600");
926 let session = BrowserSession::new(config);
927 let caps = session.build_capabilities();
928 assert!(caps.is_ok());
929 }
930}