browser_commander/core/
engine.rs

1//! Browser engine detection and abstraction.
2//!
3//! This module provides traits and types for abstracting over different
4//! browser automation engines (currently focused on Chromium-based browsers).
5
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10/// The type of browser automation engine being used.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum EngineType {
14    /// Chrome DevTools Protocol based engine (similar to Puppeteer)
15    Chromiumoxide,
16    /// WebDriver-based engine (similar to Playwright's approach)
17    Fantoccini,
18}
19
20impl std::fmt::Display for EngineType {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        match self {
23            EngineType::Chromiumoxide => write!(f, "chromiumoxide"),
24            EngineType::Fantoccini => write!(f, "fantoccini"),
25        }
26    }
27}
28
29impl std::str::FromStr for EngineType {
30    type Err = EngineError;
31
32    fn from_str(s: &str) -> Result<Self, Self::Err> {
33        match s.to_lowercase().as_str() {
34            "chromiumoxide" | "cdp" | "puppeteer" => Ok(EngineType::Chromiumoxide),
35            "fantoccini" | "webdriver" | "playwright" => Ok(EngineType::Fantoccini),
36            _ => Err(EngineError::InvalidEngine(s.to_string())),
37        }
38    }
39}
40
41/// Errors related to browser engine operations.
42#[derive(Debug, Error)]
43pub enum EngineError {
44    /// Invalid engine type specified.
45    #[error("Invalid engine: {0}. Expected 'chromiumoxide' or 'fantoccini'")]
46    InvalidEngine(String),
47
48    /// Element not found.
49    #[error("Element not found: {0}")]
50    ElementNotFound(String),
51
52    /// Operation timed out.
53    #[error("Operation timed out: {0}")]
54    Timeout(String),
55
56    /// Navigation error.
57    #[error("Navigation error: {0}")]
58    Navigation(String),
59
60    /// JavaScript evaluation error.
61    #[error("JavaScript evaluation error: {0}")]
62    Evaluation(String),
63
64    /// Generic browser error.
65    #[error("Browser error: {0}")]
66    Browser(String),
67}
68
69/// Result of an element query.
70#[derive(Debug, Clone)]
71pub struct ElementInfo {
72    /// The element's tag name.
73    pub tag_name: String,
74    /// The element's text content.
75    pub text_content: Option<String>,
76    /// Whether the element is visible.
77    pub is_visible: bool,
78    /// Whether the element is enabled (for form elements).
79    pub is_enabled: bool,
80    /// The element's bounding box (x, y, width, height).
81    pub bounding_box: Option<(f64, f64, f64, f64)>,
82}
83
84/// Result of a click verification.
85#[derive(Debug, Clone)]
86pub struct ClickVerificationResult {
87    /// Whether the click was verified as successful.
88    pub verified: bool,
89    /// The reason for the verification result.
90    pub reason: String,
91    /// Whether a navigation error occurred during verification.
92    pub navigation_error: bool,
93}
94
95/// Result of a scroll verification.
96#[derive(Debug, Clone)]
97pub struct ScrollVerificationResult {
98    /// Whether the scroll was verified as successful.
99    pub verified: bool,
100    /// Whether the element is in the viewport.
101    pub in_viewport: bool,
102    /// Number of verification attempts.
103    pub attempts: u32,
104}
105
106/// Result of a fill verification.
107#[derive(Debug, Clone)]
108pub struct FillVerificationResult {
109    /// Whether the fill was verified as successful.
110    pub verified: bool,
111    /// The actual value in the element after filling.
112    pub actual_value: String,
113    /// Number of verification attempts.
114    pub attempts: u32,
115}
116
117/// Pre-click state captured for verification.
118#[derive(Debug, Clone, Default)]
119pub struct PreClickState {
120    /// Whether the element was disabled.
121    pub disabled: Option<bool>,
122    /// The aria-pressed attribute value.
123    pub aria_pressed: Option<String>,
124    /// The aria-expanded attribute value.
125    pub aria_expanded: Option<String>,
126    /// The aria-selected attribute value.
127    pub aria_selected: Option<String>,
128    /// Whether the element was checked (for checkboxes).
129    pub checked: Option<bool>,
130    /// The element's class name.
131    pub class_name: Option<String>,
132    /// Whether the element is connected to the DOM.
133    pub is_connected: bool,
134}
135
136/// Trait for browser engine adapters.
137///
138/// This trait provides a unified interface for different browser automation
139/// engines, allowing the library to work with multiple backends.
140#[async_trait]
141pub trait EngineAdapter: Send + Sync {
142    /// Get the engine type.
143    fn engine_type(&self) -> EngineType;
144
145    /// Get the current page URL.
146    async fn url(&self) -> Result<String, EngineError>;
147
148    /// Navigate to a URL.
149    async fn goto(&self, url: &str) -> Result<(), EngineError>;
150
151    /// Query for a single element.
152    async fn query_selector(&self, selector: &str) -> Result<Option<ElementInfo>, EngineError>;
153
154    /// Query for all matching elements.
155    async fn query_selector_all(&self, selector: &str) -> Result<Vec<ElementInfo>, EngineError>;
156
157    /// Count matching elements.
158    async fn count(&self, selector: &str) -> Result<usize, EngineError>;
159
160    /// Click an element.
161    async fn click(&self, selector: &str) -> Result<(), EngineError>;
162
163    /// Fill an input element with text.
164    async fn fill(&self, selector: &str, text: &str) -> Result<(), EngineError>;
165
166    /// Type text into an element (simulating key presses).
167    async fn type_text(&self, selector: &str, text: &str) -> Result<(), EngineError>;
168
169    /// Get the text content of an element.
170    async fn text_content(&self, selector: &str) -> Result<Option<String>, EngineError>;
171
172    /// Get the value of an input element.
173    async fn input_value(&self, selector: &str) -> Result<Option<String>, EngineError>;
174
175    /// Get an attribute value from an element.
176    async fn get_attribute(
177        &self,
178        selector: &str,
179        attribute: &str,
180    ) -> Result<Option<String>, EngineError>;
181
182    /// Check if an element is visible.
183    async fn is_visible(&self, selector: &str) -> Result<bool, EngineError>;
184
185    /// Check if an element is enabled.
186    async fn is_enabled(&self, selector: &str) -> Result<bool, EngineError>;
187
188    /// Wait for a selector to appear.
189    async fn wait_for_selector(&self, selector: &str, timeout_ms: u64) -> Result<(), EngineError>;
190
191    /// Scroll an element into view.
192    async fn scroll_into_view(&self, selector: &str) -> Result<(), EngineError>;
193
194    /// Evaluate JavaScript in the page context.
195    async fn evaluate(&self, script: &str) -> Result<serde_json::Value, EngineError>;
196
197    /// Take a screenshot.
198    async fn screenshot(&self) -> Result<Vec<u8>, EngineError>;
199
200    /// Bring the page to front.
201    async fn bring_to_front(&self) -> Result<(), EngineError>;
202
203    /// Wait for navigation to complete.
204    async fn wait_for_navigation(&self, timeout_ms: u64) -> Result<(), EngineError>;
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn engine_type_display() {
213        assert_eq!(EngineType::Chromiumoxide.to_string(), "chromiumoxide");
214        assert_eq!(EngineType::Fantoccini.to_string(), "fantoccini");
215    }
216
217    #[test]
218    fn engine_type_from_str() {
219        assert_eq!(
220            "chromiumoxide".parse::<EngineType>().unwrap(),
221            EngineType::Chromiumoxide
222        );
223        assert_eq!(
224            "cdp".parse::<EngineType>().unwrap(),
225            EngineType::Chromiumoxide
226        );
227        assert_eq!(
228            "puppeteer".parse::<EngineType>().unwrap(),
229            EngineType::Chromiumoxide
230        );
231        assert_eq!(
232            "fantoccini".parse::<EngineType>().unwrap(),
233            EngineType::Fantoccini
234        );
235        assert_eq!(
236            "webdriver".parse::<EngineType>().unwrap(),
237            EngineType::Fantoccini
238        );
239        assert_eq!(
240            "playwright".parse::<EngineType>().unwrap(),
241            EngineType::Fantoccini
242        );
243    }
244
245    #[test]
246    fn engine_type_from_str_case_insensitive() {
247        assert_eq!(
248            "CHROMIUMOXIDE".parse::<EngineType>().unwrap(),
249            EngineType::Chromiumoxide
250        );
251        assert_eq!(
252            "Fantoccini".parse::<EngineType>().unwrap(),
253            EngineType::Fantoccini
254        );
255    }
256
257    #[test]
258    fn engine_type_from_str_invalid() {
259        let result = "invalid".parse::<EngineType>();
260        assert!(result.is_err());
261        if let Err(EngineError::InvalidEngine(name)) = result {
262            assert_eq!(name, "invalid");
263        } else {
264            panic!("Expected InvalidEngine error");
265        }
266    }
267
268    #[test]
269    fn pre_click_state_default() {
270        let state = PreClickState::default();
271        assert!(state.disabled.is_none());
272        assert!(state.aria_pressed.is_none());
273        assert!(!state.is_connected);
274    }
275
276    #[test]
277    fn click_verification_result_creation() {
278        let result = ClickVerificationResult {
279            verified: true,
280            reason: "element state changed".to_string(),
281            navigation_error: false,
282        };
283        assert!(result.verified);
284        assert!(!result.navigation_error);
285    }
286}