Skip to main content

jugar_probar/
driver.rs

1//! ProbarDriver - Abstract Browser Automation Trait
2//!
3//! Per spec Section 3.2: Full Chromium automation via abstract `ProbarDriver` trait.
4//!
5//! # Architecture (from spec)
6//!
7//! ```text
8//! ┌───────────────────────────────────────────────────────────────────────────┐
9//! │  ProbarDriver (Abstract Trait)                                             │
10//! │  Genchi Genbutsu: "Go and see" - allows swapping implementations          │
11//! ├───────────────────────────────────────────────────────────────────────────┤
12//! │                                                                           │
13//! │  ┌─────────────────────┐  ┌─────────────────────┐  ┌─────────────────┐  │
14//! │  │  ChromiumDriver     │  │  PlaywrightBridge   │  │  WasmBindgen    │  │
15//! │  │  (Default)          │  │  (Fallback)         │  │  (Unit Tests)   │  │
16//! │  │                     │  │                     │  │                 │  │
17//! │  │  Uses CDP via       │  │  WebSocket to       │  │  In-browser     │  │
18//! │  │  chromiumoxide      │  │  Playwright server  │  │  test runner    │  │
19//! │  └─────────────────────┘  └─────────────────────┘  └─────────────────┘  │
20//! │                                                                           │
21//! │  Toyota Principle: Driver Abstraction protects against API instability   │
22//! └───────────────────────────────────────────────────────────────────────────┘
23//! ```
24//!
25//! # Toyota Principles Applied
26//!
27//! - **Genchi Genbutsu**: Abstract trait allows "going and seeing" with different browsers
28//! - **Risk Mitigation**: If chromiumoxide becomes unmaintained, swap to PlaywrightBridge
29
30#[cfg(feature = "browser")]
31use crate::event::InputEvent;
32use crate::locator::BoundingBox;
33#[cfg(feature = "browser")]
34use crate::result::{ProbarError, ProbarResult};
35use serde::{Deserialize, Serialize};
36use std::time::Duration;
37
38#[cfg(feature = "browser")]
39use async_trait::async_trait;
40
41/// Element handle for DOM interactions
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ElementHandle {
44    /// Unique identifier for the element
45    pub id: String,
46    /// Element tag name
47    pub tag_name: String,
48    /// Element text content
49    pub text_content: Option<String>,
50    /// Bounding box if visible
51    pub bounding_box: Option<BoundingBox>,
52}
53
54impl ElementHandle {
55    /// Create a new element handle
56    #[must_use]
57    pub fn new(id: impl Into<String>, tag_name: impl Into<String>) -> Self {
58        Self {
59            id: id.into(),
60            tag_name: tag_name.into(),
61            text_content: None,
62            bounding_box: None,
63        }
64    }
65
66    /// Check if element is visible
67    #[must_use]
68    pub const fn is_visible(&self) -> bool {
69        self.bounding_box.is_some()
70    }
71}
72
73/// Page performance metrics
74#[derive(Debug, Clone, Default, Serialize, Deserialize)]
75pub struct PageMetrics {
76    /// Time to first paint (ms)
77    pub first_paint_ms: Option<f64>,
78    /// Time to first contentful paint (ms)
79    pub first_contentful_paint_ms: Option<f64>,
80    /// DOM content loaded time (ms)
81    pub dom_content_loaded_ms: Option<f64>,
82    /// Full page load time (ms)
83    pub load_time_ms: Option<f64>,
84    /// Total JavaScript heap size (bytes)
85    pub js_heap_size_bytes: Option<u64>,
86    /// Used JavaScript heap size (bytes)
87    pub js_heap_used_bytes: Option<u64>,
88    /// Number of DOM nodes
89    pub dom_nodes: Option<u32>,
90    /// Number of frames
91    pub frame_count: Option<u32>,
92}
93
94/// Screenshot data with metadata
95#[derive(Debug, Clone)]
96pub struct Screenshot {
97    /// Raw PNG data
98    pub data: Vec<u8>,
99    /// Width in pixels
100    pub width: u32,
101    /// Height in pixels
102    pub height: u32,
103    /// Device pixel ratio
104    pub device_pixel_ratio: f64,
105    /// Timestamp when screenshot was taken
106    pub timestamp: std::time::SystemTime,
107}
108
109impl Screenshot {
110    /// Create a new screenshot
111    #[must_use]
112    pub fn new(data: Vec<u8>, width: u32, height: u32) -> Self {
113        Self {
114            data,
115            width,
116            height,
117            device_pixel_ratio: 1.0,
118            timestamp: std::time::SystemTime::now(),
119        }
120    }
121
122    /// Get the size in bytes
123    #[must_use]
124    pub fn size_bytes(&self) -> usize {
125        self.data.len()
126    }
127
128    /// Check if screenshot is valid (has data)
129    #[must_use]
130    pub fn is_valid(&self) -> bool {
131        !self.data.is_empty() && self.width > 0 && self.height > 0
132    }
133}
134
135/// Network request interceptor configuration
136#[derive(Debug, Clone, Default)]
137pub struct NetworkInterceptor {
138    /// URL patterns to intercept
139    pub patterns: Vec<String>,
140    /// Whether to block matching requests
141    pub block: bool,
142    /// Response to return (if overriding)
143    pub response_override: Option<NetworkResponse>,
144}
145
146/// Mock network response
147#[derive(Debug, Clone)]
148pub struct NetworkResponse {
149    /// HTTP status code
150    pub status: u16,
151    /// Response headers
152    pub headers: Vec<(String, String)>,
153    /// Response body
154    pub body: Vec<u8>,
155}
156
157impl NetworkResponse {
158    /// Create a JSON response
159    #[must_use]
160    pub fn json(status: u16, body: impl Serialize) -> Self {
161        let body = serde_json::to_vec(&body).unwrap_or_default();
162        Self {
163            status,
164            headers: vec![("Content-Type".to_string(), "application/json".to_string())],
165            body,
166        }
167    }
168
169    /// Create a 404 response
170    #[must_use]
171    pub fn not_found() -> Self {
172        Self {
173            status: 404,
174            headers: vec![],
175            body: b"Not Found".to_vec(),
176        }
177    }
178}
179
180/// Browser configuration for driver
181#[derive(Debug, Clone)]
182pub struct DriverConfig {
183    /// Run in headless mode
184    pub headless: bool,
185    /// Viewport width
186    pub viewport_width: u32,
187    /// Viewport height
188    pub viewport_height: u32,
189    /// Device scale factor
190    pub device_scale_factor: f64,
191    /// User agent string
192    pub user_agent: Option<String>,
193    /// Timeout for navigation
194    pub navigation_timeout: Duration,
195    /// Timeout for element queries
196    pub element_timeout: Duration,
197    /// Enable tracing
198    pub tracing: bool,
199    /// Executable path override
200    pub executable_path: Option<String>,
201}
202
203impl Default for DriverConfig {
204    fn default() -> Self {
205        Self {
206            headless: true,
207            viewport_width: 1920,
208            viewport_height: 1080,
209            device_scale_factor: 1.0,
210            user_agent: None,
211            navigation_timeout: Duration::from_secs(30),
212            element_timeout: Duration::from_secs(5),
213            tracing: false,
214            executable_path: None,
215        }
216    }
217}
218
219impl DriverConfig {
220    /// Create new config with defaults
221    #[must_use]
222    pub fn new() -> Self {
223        Self::default()
224    }
225
226    /// Set headless mode
227    #[must_use]
228    pub const fn headless(mut self, headless: bool) -> Self {
229        self.headless = headless;
230        self
231    }
232
233    /// Set viewport dimensions
234    #[must_use]
235    pub const fn viewport(mut self, width: u32, height: u32) -> Self {
236        self.viewport_width = width;
237        self.viewport_height = height;
238        self
239    }
240
241    /// Set device scale factor
242    #[must_use]
243    pub const fn scale_factor(mut self, factor: f64) -> Self {
244        self.device_scale_factor = factor;
245        self
246    }
247
248    /// Set user agent
249    #[must_use]
250    pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
251        self.user_agent = Some(ua.into());
252        self
253    }
254
255    /// Set navigation timeout
256    #[must_use]
257    pub const fn navigation_timeout(mut self, timeout: Duration) -> Self {
258        self.navigation_timeout = timeout;
259        self
260    }
261
262    /// Enable tracing
263    #[must_use]
264    pub const fn with_tracing(mut self, enabled: bool) -> Self {
265        self.tracing = enabled;
266        self
267    }
268}
269
270/// Mobile device descriptor for emulation
271#[derive(Debug, Clone)]
272pub struct DeviceDescriptor {
273    /// Device name
274    pub name: &'static str,
275    /// Viewport width
276    pub viewport_width: u32,
277    /// Viewport height
278    pub viewport_height: u32,
279    /// Device scale factor
280    pub device_scale_factor: f64,
281    /// Is mobile device
282    pub is_mobile: bool,
283    /// Has touch support
284    pub has_touch: bool,
285    /// Default user agent
286    pub user_agent: &'static str,
287}
288
289impl DeviceDescriptor {
290    /// iPhone 14 Pro
291    pub const IPHONE_14_PRO: Self = Self {
292        name: "iPhone 14 Pro",
293        viewport_width: 393,
294        viewport_height: 852,
295        device_scale_factor: 3.0,
296        is_mobile: true,
297        has_touch: true,
298        user_agent: "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15",
299    };
300
301    /// iPad Pro 12.9
302    pub const IPAD_PRO_12_9: Self = Self {
303        name: "iPad Pro 12.9",
304        viewport_width: 1024,
305        viewport_height: 1366,
306        device_scale_factor: 2.0,
307        is_mobile: true,
308        has_touch: true,
309        user_agent: "Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15",
310    };
311
312    /// Desktop 1080p
313    pub const DESKTOP_1080P: Self = Self {
314        name: "Desktop 1080p",
315        viewport_width: 1920,
316        viewport_height: 1080,
317        device_scale_factor: 1.0,
318        is_mobile: false,
319        has_touch: false,
320        user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0",
321    };
322
323    /// Desktop 4K
324    pub const DESKTOP_4K: Self = Self {
325        name: "Desktop 4K",
326        viewport_width: 3840,
327        viewport_height: 2160,
328        device_scale_factor: 2.0,
329        is_mobile: false,
330        has_touch: false,
331        user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0",
332    };
333
334    /// Convert to driver config
335    #[must_use]
336    pub fn to_config(&self) -> DriverConfig {
337        DriverConfig::default()
338            .viewport(self.viewport_width, self.viewport_height)
339            .scale_factor(self.device_scale_factor)
340            .user_agent(self.user_agent)
341    }
342}
343
344/// Abstract driver trait for browser automation
345///
346/// This trait allows swapping implementations (Genchi Genbutsu principle).
347/// If chromiumoxide becomes unmaintained, implement `PlaywrightBridgeDriver`.
348///
349/// # Implementations
350///
351/// - `ChromiumDriver` - Default, uses chromiumoxide crate
352/// - `PlaywrightBridgeDriver` - Fallback, WebSocket to Playwright server
353/// - `MockDriver` - For unit testing
354#[cfg(feature = "browser")]
355#[async_trait]
356pub trait ProbarDriver: Send + Sync {
357    /// Navigate to URL
358    async fn navigate(&mut self, url: &str) -> ProbarResult<()>;
359
360    /// Take screenshot
361    async fn screenshot(&self) -> ProbarResult<Screenshot>;
362
363    /// Execute JavaScript in page context
364    async fn execute_js(&self, script: &str) -> ProbarResult<serde_json::Value>;
365
366    /// Query DOM element by selector
367    async fn query_selector(&self, selector: &str) -> ProbarResult<Option<ElementHandle>>;
368
369    /// Query all matching elements
370    async fn query_selector_all(&self, selector: &str) -> ProbarResult<Vec<ElementHandle>>;
371
372    /// Dispatch input event
373    async fn dispatch_input(&self, event: InputEvent) -> ProbarResult<()>;
374
375    /// Click element
376    async fn click(&self, selector: &str) -> ProbarResult<()>;
377
378    /// Type text into element
379    async fn type_text(&self, selector: &str, text: &str) -> ProbarResult<()>;
380
381    /// Wait for selector to appear
382    async fn wait_for_selector(
383        &self,
384        selector: &str,
385        timeout: Duration,
386    ) -> ProbarResult<ElementHandle>;
387
388    /// Get page metrics
389    async fn metrics(&self) -> ProbarResult<PageMetrics>;
390
391    /// Set network interceptor
392    async fn set_network_interceptor(
393        &mut self,
394        interceptor: NetworkInterceptor,
395    ) -> ProbarResult<()>;
396
397    /// Get current URL
398    async fn current_url(&self) -> ProbarResult<String>;
399
400    /// Go back in history
401    async fn go_back(&mut self) -> ProbarResult<()>;
402
403    /// Go forward in history
404    async fn go_forward(&mut self) -> ProbarResult<()>;
405
406    /// Reload page
407    async fn reload(&mut self) -> ProbarResult<()>;
408
409    /// Close the browser
410    async fn close(&mut self) -> ProbarResult<()>;
411}
412
413/// Mock driver for unit testing
414#[derive(Debug, Default)]
415pub struct MockDriver {
416    /// Current URL
417    pub current_url: String,
418    /// Mock elements
419    pub elements: Vec<ElementHandle>,
420    /// JS execution results
421    pub js_results: Vec<serde_json::Value>,
422    /// Screenshot data
423    pub screenshot_data: Option<Screenshot>,
424    /// Call history for verification
425    pub call_history: Vec<String>,
426}
427
428impl MockDriver {
429    /// Create new mock driver
430    #[must_use]
431    pub fn new() -> Self {
432        Self::default()
433    }
434
435    /// Add a mock element
436    pub fn add_element(&mut self, element: ElementHandle) {
437        self.elements.push(element);
438    }
439
440    /// Set mock JS result
441    pub fn set_js_result(&mut self, result: serde_json::Value) {
442        self.js_results.push(result);
443    }
444
445    /// Set mock screenshot
446    pub fn set_screenshot(&mut self, screenshot: Screenshot) {
447        self.screenshot_data = Some(screenshot);
448    }
449
450    /// Get call history
451    #[must_use]
452    pub fn history(&self) -> &[String] {
453        &self.call_history
454    }
455
456    /// Check if method was called
457    #[must_use]
458    pub fn was_called(&self, method: &str) -> bool {
459        self.call_history.iter().any(|c| c.starts_with(method))
460    }
461}
462
463#[cfg(feature = "browser")]
464#[async_trait]
465impl ProbarDriver for MockDriver {
466    async fn navigate(&mut self, url: &str) -> ProbarResult<()> {
467        self.call_history.push(format!("navigate:{url}"));
468        self.current_url = url.to_string();
469        Ok(())
470    }
471
472    async fn screenshot(&self) -> ProbarResult<Screenshot> {
473        self.screenshot_data
474            .clone()
475            .ok_or_else(|| ProbarError::ScreenshotError {
476                message: "No mock screenshot set".to_string(),
477            })
478    }
479
480    async fn execute_js(&self, script: &str) -> ProbarResult<serde_json::Value> {
481        let _ = script; // Unused in mock
482        self.js_results
483            .first()
484            .cloned()
485            .ok_or_else(|| ProbarError::WasmError {
486                message: "No mock JS result set".to_string(),
487            })
488    }
489
490    async fn query_selector(&self, selector: &str) -> ProbarResult<Option<ElementHandle>> {
491        Ok(self.elements.iter().find(|e| e.id == selector).cloned())
492    }
493
494    async fn query_selector_all(&self, _selector: &str) -> ProbarResult<Vec<ElementHandle>> {
495        Ok(self.elements.clone())
496    }
497
498    async fn dispatch_input(&self, event: InputEvent) -> ProbarResult<()> {
499        let _ = event; // Mock just records the call
500        Ok(())
501    }
502
503    async fn click(&self, selector: &str) -> ProbarResult<()> {
504        let _ = selector;
505        Ok(())
506    }
507
508    async fn type_text(&self, selector: &str, text: &str) -> ProbarResult<()> {
509        let _ = (selector, text);
510        Ok(())
511    }
512
513    async fn wait_for_selector(
514        &self,
515        selector: &str,
516        _timeout: Duration,
517    ) -> ProbarResult<ElementHandle> {
518        self.elements
519            .iter()
520            .find(|e| e.id == selector)
521            .cloned()
522            .ok_or_else(|| ProbarError::Timeout { ms: 5000 })
523    }
524
525    async fn metrics(&self) -> ProbarResult<PageMetrics> {
526        Ok(PageMetrics::default())
527    }
528
529    async fn set_network_interceptor(
530        &mut self,
531        _interceptor: NetworkInterceptor,
532    ) -> ProbarResult<()> {
533        Ok(())
534    }
535
536    async fn current_url(&self) -> ProbarResult<String> {
537        Ok(self.current_url.clone())
538    }
539
540    async fn go_back(&mut self) -> ProbarResult<()> {
541        self.call_history.push("go_back".to_string());
542        Ok(())
543    }
544
545    async fn go_forward(&mut self) -> ProbarResult<()> {
546        self.call_history.push("go_forward".to_string());
547        Ok(())
548    }
549
550    async fn reload(&mut self) -> ProbarResult<()> {
551        self.call_history.push("reload".to_string());
552        Ok(())
553    }
554
555    async fn close(&mut self) -> ProbarResult<()> {
556        self.call_history.push("close".to_string());
557        Ok(())
558    }
559}
560
561/// Browser controller using abstract driver
562///
563/// This is the main entry point for browser automation tests.
564///
565/// # Example
566///
567/// ```ignore
568/// let mut browser = BrowserController::<ChromiumDriver>::launch(
569///     DriverConfig::default().headless(true)
570/// ).await?;
571///
572/// browser.goto("http://localhost:8080/game").await?;
573/// let screenshot = browser.screenshot().await?;
574/// ```
575#[cfg(feature = "browser")]
576pub struct BrowserController<D: ProbarDriver> {
577    driver: D,
578    config: DriverConfig,
579}
580
581#[cfg(feature = "browser")]
582impl<D: ProbarDriver> std::fmt::Debug for BrowserController<D> {
583    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
584        f.debug_struct("BrowserController")
585            .field("config", &self.config)
586            .finish_non_exhaustive()
587    }
588}
589
590#[cfg(feature = "browser")]
591impl<D: ProbarDriver> BrowserController<D> {
592    /// Create controller with existing driver
593    pub fn new(driver: D, config: DriverConfig) -> Self {
594        Self { driver, config }
595    }
596
597    /// Navigate to URL
598    pub async fn goto(&mut self, url: &str) -> ProbarResult<()> {
599        self.driver.navigate(url).await
600    }
601
602    /// Take screenshot
603    pub async fn screenshot(&self) -> ProbarResult<Screenshot> {
604        self.driver.screenshot().await
605    }
606
607    /// Execute JavaScript
608    pub async fn evaluate(&self, script: &str) -> ProbarResult<serde_json::Value> {
609        self.driver.execute_js(script).await
610    }
611
612    /// Query element
613    pub async fn query(&self, selector: &str) -> ProbarResult<Option<ElementHandle>> {
614        self.driver.query_selector(selector).await
615    }
616
617    /// Get page metrics
618    pub async fn metrics(&self) -> ProbarResult<PageMetrics> {
619        self.driver.metrics().await
620    }
621
622    /// Get configuration
623    #[must_use]
624    pub const fn config(&self) -> &DriverConfig {
625        &self.config
626    }
627
628    /// Close browser
629    pub async fn close(&mut self) -> ProbarResult<()> {
630        self.driver.close().await
631    }
632}
633
634// ============================================================================
635// EXTREME TDD: Tests written FIRST per spec Section 6.1
636// ============================================================================
637
638#[cfg(test)]
639#[allow(clippy::unwrap_used, clippy::expect_used)]
640mod tests {
641    use super::*;
642
643    mod element_handle_tests {
644        use super::*;
645
646        #[test]
647        fn test_element_handle_creation() {
648            let elem = ElementHandle::new("btn-1", "button");
649            assert_eq!(elem.id, "btn-1");
650            assert_eq!(elem.tag_name, "button");
651            assert!(elem.text_content.is_none());
652        }
653
654        #[test]
655        fn test_element_handle_visibility() {
656            let mut elem = ElementHandle::new("elem", "div");
657            assert!(!elem.is_visible());
658
659            elem.bounding_box = Some(BoundingBox::new(0.0, 0.0, 100.0, 100.0));
660            assert!(elem.is_visible());
661        }
662    }
663
664    mod screenshot_tests {
665        use super::*;
666
667        #[test]
668        fn test_screenshot_creation() {
669            let data = vec![0x89, 0x50, 0x4E, 0x47]; // PNG magic bytes
670            let screenshot = Screenshot::new(data.clone(), 100, 100);
671            assert_eq!(screenshot.width, 100);
672            assert_eq!(screenshot.height, 100);
673            assert_eq!(screenshot.data, data);
674        }
675
676        #[test]
677        fn test_screenshot_size_bytes() {
678            let screenshot = Screenshot::new(vec![0; 1024], 100, 100);
679            assert_eq!(screenshot.size_bytes(), 1024);
680        }
681
682        #[test]
683        fn test_screenshot_is_valid() {
684            let valid = Screenshot::new(vec![1, 2, 3], 100, 100);
685            assert!(valid.is_valid());
686
687            let empty = Screenshot::new(vec![], 100, 100);
688            assert!(!empty.is_valid());
689
690            let zero_width = Screenshot::new(vec![1], 0, 100);
691            assert!(!zero_width.is_valid());
692        }
693    }
694
695    mod network_response_tests {
696        use super::*;
697
698        #[test]
699        fn test_json_response() {
700            let response = NetworkResponse::json(200, serde_json::json!({"ok": true}));
701            assert_eq!(response.status, 200);
702            assert!(response
703                .headers
704                .iter()
705                .any(|(k, v)| k == "Content-Type" && v.contains("json")));
706        }
707
708        #[test]
709        fn test_not_found_response() {
710            let response = NetworkResponse::not_found();
711            assert_eq!(response.status, 404);
712        }
713    }
714
715    mod driver_config_tests {
716        use super::*;
717
718        #[test]
719        fn test_config_default() {
720            let config = DriverConfig::default();
721            assert!(config.headless);
722            assert_eq!(config.viewport_width, 1920);
723            assert_eq!(config.viewport_height, 1080);
724        }
725
726        #[test]
727        fn test_config_builder() {
728            let config = DriverConfig::new()
729                .headless(false)
730                .viewport(800, 600)
731                .scale_factor(2.0)
732                .user_agent("test-agent");
733
734            assert!(!config.headless);
735            assert_eq!(config.viewport_width, 800);
736            assert_eq!(config.viewport_height, 600);
737            assert!((config.device_scale_factor - 2.0).abs() < f64::EPSILON);
738            assert_eq!(config.user_agent, Some("test-agent".to_string()));
739        }
740
741        #[test]
742        fn test_config_with_tracing() {
743            let config = DriverConfig::new().with_tracing(true);
744            assert!(config.tracing);
745        }
746    }
747
748    mod device_descriptor_tests {
749        use super::*;
750
751        #[test]
752        fn test_iphone_14_pro() {
753            let device = DeviceDescriptor::IPHONE_14_PRO;
754            assert_eq!(device.viewport_width, 393);
755            assert!(device.is_mobile);
756            assert!(device.has_touch);
757        }
758
759        #[test]
760        fn test_desktop_1080p() {
761            let device = DeviceDescriptor::DESKTOP_1080P;
762            assert_eq!(device.viewport_width, 1920);
763            assert!(!device.is_mobile);
764            assert!(!device.has_touch);
765        }
766
767        #[test]
768        fn test_device_to_config() {
769            let config = DeviceDescriptor::IPHONE_14_PRO.to_config();
770            assert_eq!(config.viewport_width, 393);
771            assert_eq!(config.viewport_height, 852);
772        }
773    }
774
775    mod mock_driver_tests {
776        use super::*;
777
778        #[test]
779        fn test_mock_driver_creation() {
780            let driver = MockDriver::new();
781            assert!(driver.elements.is_empty());
782            assert!(driver.current_url.is_empty());
783        }
784
785        #[test]
786        fn test_mock_driver_add_element() {
787            let mut driver = MockDriver::new();
788            driver.add_element(ElementHandle::new("test", "div"));
789            assert_eq!(driver.elements.len(), 1);
790        }
791
792        #[test]
793        fn test_mock_driver_history() {
794            let driver = MockDriver::new();
795            assert!(driver.history().is_empty());
796        }
797
798        #[test]
799        fn test_mock_driver_was_called() {
800            let driver = MockDriver::new();
801            assert!(!driver.was_called("navigate"));
802        }
803    }
804
805    mod page_metrics_tests {
806        use super::*;
807
808        #[test]
809        fn test_metrics_default() {
810            let metrics = PageMetrics::default();
811            assert!(metrics.first_paint_ms.is_none());
812            assert!(metrics.dom_nodes.is_none());
813        }
814    }
815
816    #[cfg(feature = "browser")]
817    mod async_driver_tests {
818        use super::*;
819
820        #[tokio::test]
821        async fn test_mock_driver_navigate() {
822            let mut driver = MockDriver::new();
823            driver.navigate("https://example.com").await.unwrap();
824            assert_eq!(driver.current_url, "https://example.com");
825            assert!(driver.was_called("navigate"));
826        }
827
828        #[tokio::test]
829        async fn test_mock_driver_history_tracking() {
830            let mut driver = MockDriver::new();
831            driver.go_back().await.unwrap();
832            driver.go_forward().await.unwrap();
833            driver.reload().await.unwrap();
834            driver.close().await.unwrap();
835
836            assert!(driver.was_called("go_back"));
837            assert!(driver.was_called("go_forward"));
838            assert!(driver.was_called("reload"));
839            assert!(driver.was_called("close"));
840        }
841
842        #[tokio::test]
843        async fn test_browser_controller_with_mock() {
844            let driver = MockDriver::new();
845            let mut controller = BrowserController::new(driver, DriverConfig::default());
846
847            controller.goto("https://test.com").await.unwrap();
848            let url = controller.driver.current_url().await.unwrap();
849            assert_eq!(url, "https://test.com");
850        }
851
852        #[tokio::test]
853        async fn test_mock_driver_screenshot_error() {
854            let driver = MockDriver::new();
855            // No screenshot set, should return error
856            let result = driver.screenshot().await;
857            assert!(result.is_err());
858        }
859
860        #[tokio::test]
861        async fn test_mock_driver_screenshot_success() {
862            let mut driver = MockDriver::new();
863            let screenshot = Screenshot::new(vec![1, 2, 3], 100, 50);
864            driver.set_screenshot(screenshot);
865            let result = driver.screenshot().await.unwrap();
866            assert_eq!(result.width, 100);
867            assert_eq!(result.height, 50);
868        }
869
870        #[tokio::test]
871        async fn test_mock_driver_execute_js_error() {
872            let driver = MockDriver::new();
873            // No JS result set, should return error
874            let result = driver.execute_js("return 1;").await;
875            assert!(result.is_err());
876        }
877
878        #[tokio::test]
879        async fn test_mock_driver_execute_js_success() {
880            let mut driver = MockDriver::new();
881            driver.set_js_result(serde_json::json!({"result": 42}));
882            let result = driver.execute_js("return 42;").await.unwrap();
883            assert_eq!(result["result"], 42);
884        }
885
886        #[tokio::test]
887        async fn test_mock_driver_query_selector() {
888            let mut driver = MockDriver::new();
889            driver.add_element(ElementHandle::new("btn-submit", "button"));
890
891            let found = driver.query_selector("btn-submit").await.unwrap();
892            assert!(found.is_some());
893            assert_eq!(found.unwrap().tag_name, "button");
894
895            let not_found = driver.query_selector("non-existent").await.unwrap();
896            assert!(not_found.is_none());
897        }
898
899        #[tokio::test]
900        async fn test_mock_driver_query_selector_all() {
901            let mut driver = MockDriver::new();
902            driver.add_element(ElementHandle::new("elem1", "div"));
903            driver.add_element(ElementHandle::new("elem2", "span"));
904
905            let elements = driver.query_selector_all("*").await.unwrap();
906            assert_eq!(elements.len(), 2);
907        }
908
909        #[tokio::test]
910        async fn test_mock_driver_dispatch_input() {
911            let driver = MockDriver::new();
912            let event = InputEvent::KeyPress {
913                key: "Enter".to_string(),
914            };
915            let result = driver.dispatch_input(event).await;
916            assert!(result.is_ok());
917        }
918
919        #[tokio::test]
920        async fn test_mock_driver_click() {
921            let driver = MockDriver::new();
922            let result = driver.click("#button").await;
923            assert!(result.is_ok());
924        }
925
926        #[tokio::test]
927        async fn test_mock_driver_type_text() {
928            let driver = MockDriver::new();
929            let result = driver.type_text("#input", "hello world").await;
930            assert!(result.is_ok());
931        }
932
933        #[tokio::test]
934        async fn test_mock_driver_wait_for_selector_found() {
935            let mut driver = MockDriver::new();
936            driver.add_element(ElementHandle::new("target", "div"));
937            let result = driver
938                .wait_for_selector("target", Duration::from_secs(1))
939                .await;
940            assert!(result.is_ok());
941            assert_eq!(result.unwrap().id, "target");
942        }
943
944        #[tokio::test]
945        async fn test_mock_driver_wait_for_selector_not_found() {
946            let driver = MockDriver::new();
947            let result = driver
948                .wait_for_selector("missing", Duration::from_secs(1))
949                .await;
950            assert!(result.is_err());
951        }
952
953        #[tokio::test]
954        async fn test_mock_driver_metrics() {
955            let driver = MockDriver::new();
956            let metrics = driver.metrics().await.unwrap();
957            assert!(metrics.first_paint_ms.is_none());
958        }
959
960        #[tokio::test]
961        async fn test_mock_driver_set_network_interceptor() {
962            let mut driver = MockDriver::new();
963            let interceptor = NetworkInterceptor {
964                patterns: vec!["*.js".to_string()],
965                block: true,
966                response_override: None,
967            };
968            let result = driver.set_network_interceptor(interceptor).await;
969            assert!(result.is_ok());
970        }
971
972        #[tokio::test]
973        async fn test_browser_controller_evaluate() {
974            let mut driver = MockDriver::new();
975            driver.set_js_result(serde_json::json!({"foo": "bar"}));
976            let controller = BrowserController::new(driver, DriverConfig::default());
977            let result = controller.evaluate("return {foo: 'bar'}").await.unwrap();
978            assert_eq!(result["foo"], "bar");
979        }
980
981        #[tokio::test]
982        async fn test_browser_controller_query() {
983            let mut driver = MockDriver::new();
984            driver.add_element(ElementHandle::new("elem", "div"));
985            let controller = BrowserController::new(driver, DriverConfig::default());
986            let result = controller.query("elem").await.unwrap();
987            assert!(result.is_some());
988        }
989
990        #[tokio::test]
991        async fn test_browser_controller_metrics() {
992            let driver = MockDriver::new();
993            let controller = BrowserController::new(driver, DriverConfig::default());
994            let metrics = controller.metrics().await.unwrap();
995            assert!(metrics.first_paint_ms.is_none());
996        }
997
998        #[tokio::test]
999        async fn test_browser_controller_screenshot() {
1000            let mut driver = MockDriver::new();
1001            driver.set_screenshot(Screenshot::new(vec![0x89, 0x50], 640, 480));
1002            let controller = BrowserController::new(driver, DriverConfig::default());
1003            let screenshot = controller.screenshot().await.unwrap();
1004            assert_eq!(screenshot.width, 640);
1005            assert_eq!(screenshot.height, 480);
1006        }
1007
1008        #[tokio::test]
1009        async fn test_browser_controller_close() {
1010            let driver = MockDriver::new();
1011            let mut controller = BrowserController::new(driver, DriverConfig::default());
1012            let result = controller.close().await;
1013            assert!(result.is_ok());
1014        }
1015
1016        #[tokio::test]
1017        async fn test_browser_controller_config() {
1018            let config = DriverConfig::new().viewport(800, 600);
1019            let driver = MockDriver::new();
1020            let controller = BrowserController::new(driver, config);
1021            assert_eq!(controller.config().viewport_width, 800);
1022            assert_eq!(controller.config().viewport_height, 600);
1023        }
1024
1025        #[tokio::test]
1026        async fn test_browser_controller_debug() {
1027            let driver = MockDriver::new();
1028            let controller = BrowserController::new(driver, DriverConfig::default());
1029            let debug_str = format!("{:?}", controller);
1030            assert!(debug_str.contains("BrowserController"));
1031        }
1032    }
1033
1034    mod network_interceptor_tests {
1035        use super::*;
1036
1037        #[test]
1038        fn test_network_interceptor_default() {
1039            let interceptor = NetworkInterceptor::default();
1040            assert!(interceptor.patterns.is_empty());
1041            assert!(!interceptor.block);
1042            assert!(interceptor.response_override.is_none());
1043        }
1044
1045        #[test]
1046        fn test_network_interceptor_with_patterns() {
1047            let interceptor = NetworkInterceptor {
1048                patterns: vec!["*.js".to_string(), "*.css".to_string()],
1049                block: true,
1050                response_override: None,
1051            };
1052            assert_eq!(interceptor.patterns.len(), 2);
1053            assert!(interceptor.block);
1054        }
1055
1056        #[test]
1057        fn test_network_interceptor_with_response_override() {
1058            let response = NetworkResponse::json(200, serde_json::json!({"mock": true}));
1059            let interceptor = NetworkInterceptor {
1060                patterns: vec!["api/*".to_string()],
1061                block: false,
1062                response_override: Some(response),
1063            };
1064            assert!(interceptor.response_override.is_some());
1065        }
1066    }
1067
1068    mod driver_config_extended_tests {
1069        use super::*;
1070
1071        #[test]
1072        fn test_navigation_timeout() {
1073            let config = DriverConfig::new().navigation_timeout(Duration::from_secs(60));
1074            assert_eq!(config.navigation_timeout, Duration::from_secs(60));
1075        }
1076
1077        #[test]
1078        fn test_config_executable_path() {
1079            let mut config = DriverConfig::default();
1080            config.executable_path = Some("/usr/bin/chromium".to_string());
1081            assert_eq!(
1082                config.executable_path,
1083                Some("/usr/bin/chromium".to_string())
1084            );
1085        }
1086
1087        #[test]
1088        fn test_config_element_timeout_default() {
1089            let config = DriverConfig::default();
1090            assert_eq!(config.element_timeout, Duration::from_secs(5));
1091        }
1092    }
1093
1094    mod device_descriptor_extended_tests {
1095        use super::*;
1096
1097        #[test]
1098        fn test_ipad_pro_12_9() {
1099            let device = DeviceDescriptor::IPAD_PRO_12_9;
1100            assert_eq!(device.name, "iPad Pro 12.9");
1101            assert_eq!(device.viewport_width, 1024);
1102            assert_eq!(device.viewport_height, 1366);
1103            assert!((device.device_scale_factor - 2.0).abs() < f64::EPSILON);
1104            assert!(device.is_mobile);
1105            assert!(device.has_touch);
1106            assert!(device.user_agent.contains("iPad"));
1107        }
1108
1109        #[test]
1110        fn test_desktop_4k() {
1111            let device = DeviceDescriptor::DESKTOP_4K;
1112            assert_eq!(device.name, "Desktop 4K");
1113            assert_eq!(device.viewport_width, 3840);
1114            assert_eq!(device.viewport_height, 2160);
1115            assert!((device.device_scale_factor - 2.0).abs() < f64::EPSILON);
1116            assert!(!device.is_mobile);
1117            assert!(!device.has_touch);
1118        }
1119
1120        #[test]
1121        fn test_ipad_to_config() {
1122            let config = DeviceDescriptor::IPAD_PRO_12_9.to_config();
1123            assert_eq!(config.viewport_width, 1024);
1124            assert_eq!(config.viewport_height, 1366);
1125            assert!((config.device_scale_factor - 2.0).abs() < f64::EPSILON);
1126            assert!(config.user_agent.is_some());
1127        }
1128
1129        #[test]
1130        fn test_desktop_4k_to_config() {
1131            let config = DeviceDescriptor::DESKTOP_4K.to_config();
1132            assert_eq!(config.viewport_width, 3840);
1133            assert_eq!(config.viewport_height, 2160);
1134        }
1135    }
1136
1137    mod mock_driver_extended_tests {
1138        use super::*;
1139
1140        #[test]
1141        fn test_mock_driver_set_js_result() {
1142            let mut driver = MockDriver::new();
1143            driver.set_js_result(serde_json::json!({"value": 123}));
1144            assert_eq!(driver.js_results.len(), 1);
1145            assert_eq!(driver.js_results[0]["value"], 123);
1146        }
1147
1148        #[test]
1149        fn test_mock_driver_set_screenshot() {
1150            let mut driver = MockDriver::new();
1151            let screenshot = Screenshot::new(vec![1, 2, 3, 4], 200, 100);
1152            driver.set_screenshot(screenshot);
1153            assert!(driver.screenshot_data.is_some());
1154            let data = driver.screenshot_data.unwrap();
1155            assert_eq!(data.width, 200);
1156            assert_eq!(data.height, 100);
1157        }
1158
1159        #[test]
1160        fn test_mock_driver_debug() {
1161            let driver = MockDriver::new();
1162            let debug_str = format!("{:?}", driver);
1163            assert!(debug_str.contains("MockDriver"));
1164        }
1165
1166        #[test]
1167        fn test_mock_driver_multiple_elements() {
1168            let mut driver = MockDriver::new();
1169            driver.add_element(ElementHandle::new("a", "div"));
1170            driver.add_element(ElementHandle::new("b", "span"));
1171            driver.add_element(ElementHandle::new("c", "button"));
1172            assert_eq!(driver.elements.len(), 3);
1173        }
1174    }
1175
1176    mod element_handle_extended_tests {
1177        use super::*;
1178
1179        #[test]
1180        fn test_element_handle_with_text() {
1181            let mut elem = ElementHandle::new("p1", "p");
1182            elem.text_content = Some("Hello World".to_string());
1183            assert_eq!(elem.text_content, Some("Hello World".to_string()));
1184        }
1185
1186        #[test]
1187        fn test_element_handle_serialization() {
1188            let elem = ElementHandle::new("test-id", "input");
1189            let json = serde_json::to_string(&elem).unwrap();
1190            assert!(json.contains("test-id"));
1191            assert!(json.contains("input"));
1192        }
1193
1194        #[test]
1195        fn test_element_handle_deserialization() {
1196            let json =
1197                r#"{"id":"btn","tag_name":"button","text_content":null,"bounding_box":null}"#;
1198            let elem: ElementHandle = serde_json::from_str(json).unwrap();
1199            assert_eq!(elem.id, "btn");
1200            assert_eq!(elem.tag_name, "button");
1201        }
1202
1203        #[test]
1204        fn test_element_handle_clone() {
1205            let elem = ElementHandle::new("orig", "div");
1206            let cloned = elem.clone();
1207            assert_eq!(elem.id, cloned.id);
1208            assert_eq!(elem.tag_name, cloned.tag_name);
1209        }
1210    }
1211
1212    mod screenshot_extended_tests {
1213        use super::*;
1214
1215        #[test]
1216        fn test_screenshot_device_pixel_ratio() {
1217            let mut screenshot = Screenshot::new(vec![0], 100, 100);
1218            screenshot.device_pixel_ratio = 2.0;
1219            assert!((screenshot.device_pixel_ratio - 2.0).abs() < f64::EPSILON);
1220        }
1221
1222        #[test]
1223        fn test_screenshot_timestamp() {
1224            let screenshot = Screenshot::new(vec![0], 100, 100);
1225            // Timestamp should be recent (within the last second)
1226            let now = std::time::SystemTime::now();
1227            let duration = now.duration_since(screenshot.timestamp).unwrap();
1228            assert!(duration.as_secs() < 1);
1229        }
1230
1231        #[test]
1232        fn test_screenshot_zero_height_invalid() {
1233            let screenshot = Screenshot::new(vec![1, 2], 100, 0);
1234            assert!(!screenshot.is_valid());
1235        }
1236
1237        #[test]
1238        fn test_screenshot_clone() {
1239            let screenshot = Screenshot::new(vec![1, 2, 3], 50, 50);
1240            let cloned = screenshot.clone();
1241            assert_eq!(screenshot.data, cloned.data);
1242            assert_eq!(screenshot.width, cloned.width);
1243            assert_eq!(screenshot.height, cloned.height);
1244        }
1245    }
1246
1247    mod page_metrics_extended_tests {
1248        use super::*;
1249
1250        #[test]
1251        fn test_page_metrics_with_values() {
1252            let metrics = PageMetrics {
1253                first_paint_ms: Some(100.5),
1254                first_contentful_paint_ms: Some(150.0),
1255                dom_content_loaded_ms: Some(200.0),
1256                load_time_ms: Some(500.0),
1257                js_heap_size_bytes: Some(10_000_000),
1258                js_heap_used_bytes: Some(5_000_000),
1259                dom_nodes: Some(1500),
1260                frame_count: Some(2),
1261            };
1262            assert_eq!(metrics.first_paint_ms, Some(100.5));
1263            assert_eq!(metrics.dom_nodes, Some(1500));
1264        }
1265
1266        #[test]
1267        fn test_page_metrics_serialization() {
1268            let metrics = PageMetrics::default();
1269            let json = serde_json::to_string(&metrics).unwrap();
1270            assert!(json.contains("first_paint_ms"));
1271        }
1272
1273        #[test]
1274        fn test_page_metrics_clone() {
1275            let metrics = PageMetrics {
1276                first_paint_ms: Some(50.0),
1277                ..Default::default()
1278            };
1279            let cloned = metrics.clone();
1280            assert_eq!(metrics.first_paint_ms, cloned.first_paint_ms);
1281        }
1282    }
1283
1284    mod network_response_extended_tests {
1285        use super::*;
1286
1287        #[test]
1288        fn test_network_response_json_serialization_failure() {
1289            // Create a valid JSON to ensure normal path works
1290            let response = NetworkResponse::json(200, serde_json::json!(null));
1291            assert_eq!(response.status, 200);
1292        }
1293
1294        #[test]
1295        fn test_network_response_with_headers() {
1296            let response = NetworkResponse {
1297                status: 201,
1298                headers: vec![
1299                    ("Content-Type".to_string(), "text/plain".to_string()),
1300                    ("X-Custom".to_string(), "value".to_string()),
1301                ],
1302                body: b"Created".to_vec(),
1303            };
1304            assert_eq!(response.headers.len(), 2);
1305            assert_eq!(response.body, b"Created".to_vec());
1306        }
1307
1308        #[test]
1309        fn test_network_response_not_found_body() {
1310            let response = NetworkResponse::not_found();
1311            assert_eq!(response.body, b"Not Found".to_vec());
1312            assert!(response.headers.is_empty());
1313        }
1314
1315        #[test]
1316        fn test_network_response_clone() {
1317            let response = NetworkResponse::json(200, serde_json::json!({"ok": true}));
1318            let cloned = response.clone();
1319            assert_eq!(response.status, cloned.status);
1320            assert_eq!(response.body, cloned.body);
1321        }
1322    }
1323}