1use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum EngineType {
14 Chromiumoxide,
16 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#[derive(Debug, Error)]
43pub enum EngineError {
44 #[error("Invalid engine: {0}. Expected 'chromiumoxide' or 'fantoccini'")]
46 InvalidEngine(String),
47
48 #[error("Element not found: {0}")]
50 ElementNotFound(String),
51
52 #[error("Operation timed out: {0}")]
54 Timeout(String),
55
56 #[error("Navigation error: {0}")]
58 Navigation(String),
59
60 #[error("JavaScript evaluation error: {0}")]
62 Evaluation(String),
63
64 #[error("Browser error: {0}")]
66 Browser(String),
67}
68
69#[derive(Debug, Clone)]
71pub struct ElementInfo {
72 pub tag_name: String,
74 pub text_content: Option<String>,
76 pub is_visible: bool,
78 pub is_enabled: bool,
80 pub bounding_box: Option<(f64, f64, f64, f64)>,
82}
83
84#[derive(Debug, Clone)]
86pub struct ClickVerificationResult {
87 pub verified: bool,
89 pub reason: String,
91 pub navigation_error: bool,
93}
94
95#[derive(Debug, Clone)]
97pub struct ScrollVerificationResult {
98 pub verified: bool,
100 pub in_viewport: bool,
102 pub attempts: u32,
104}
105
106#[derive(Debug, Clone)]
108pub struct FillVerificationResult {
109 pub verified: bool,
111 pub actual_value: String,
113 pub attempts: u32,
115}
116
117#[derive(Debug, Clone, Default)]
119pub struct PreClickState {
120 pub disabled: Option<bool>,
122 pub aria_pressed: Option<String>,
124 pub aria_expanded: Option<String>,
126 pub aria_selected: Option<String>,
128 pub checked: Option<bool>,
130 pub class_name: Option<String>,
132 pub is_connected: bool,
134}
135
136#[async_trait]
141pub trait EngineAdapter: Send + Sync {
142 fn engine_type(&self) -> EngineType;
144
145 async fn url(&self) -> Result<String, EngineError>;
147
148 async fn goto(&self, url: &str) -> Result<(), EngineError>;
150
151 async fn query_selector(&self, selector: &str) -> Result<Option<ElementInfo>, EngineError>;
153
154 async fn query_selector_all(&self, selector: &str) -> Result<Vec<ElementInfo>, EngineError>;
156
157 async fn count(&self, selector: &str) -> Result<usize, EngineError>;
159
160 async fn click(&self, selector: &str) -> Result<(), EngineError>;
162
163 async fn fill(&self, selector: &str, text: &str) -> Result<(), EngineError>;
165
166 async fn type_text(&self, selector: &str, text: &str) -> Result<(), EngineError>;
168
169 async fn text_content(&self, selector: &str) -> Result<Option<String>, EngineError>;
171
172 async fn input_value(&self, selector: &str) -> Result<Option<String>, EngineError>;
174
175 async fn get_attribute(
177 &self,
178 selector: &str,
179 attribute: &str,
180 ) -> Result<Option<String>, EngineError>;
181
182 async fn is_visible(&self, selector: &str) -> Result<bool, EngineError>;
184
185 async fn is_enabled(&self, selector: &str) -> Result<bool, EngineError>;
187
188 async fn wait_for_selector(&self, selector: &str, timeout_ms: u64) -> Result<(), EngineError>;
190
191 async fn scroll_into_view(&self, selector: &str) -> Result<(), EngineError>;
193
194 async fn evaluate(&self, script: &str) -> Result<serde_json::Value, EngineError>;
196
197 async fn screenshot(&self) -> Result<Vec<u8>, EngineError>;
199
200 async fn bring_to_front(&self) -> Result<(), EngineError>;
202
203 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}