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 Playwright,
20 Puppeteer,
22}
23
24impl std::fmt::Display for EngineType {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 match self {
27 EngineType::Chromiumoxide => write!(f, "chromiumoxide"),
28 EngineType::Fantoccini => write!(f, "fantoccini"),
29 EngineType::Playwright => write!(f, "playwright"),
30 EngineType::Puppeteer => write!(f, "puppeteer"),
31 }
32 }
33}
34
35impl std::str::FromStr for EngineType {
36 type Err = EngineError;
37
38 fn from_str(s: &str) -> Result<Self, Self::Err> {
39 match s.to_lowercase().as_str() {
40 "chromiumoxide" | "cdp" => Ok(EngineType::Chromiumoxide),
41 "fantoccini" | "webdriver" => Ok(EngineType::Fantoccini),
42 "playwright" => Ok(EngineType::Playwright),
43 "puppeteer" => Ok(EngineType::Puppeteer),
44 _ => Err(EngineError::InvalidEngine(s.to_string())),
45 }
46 }
47}
48
49#[derive(Debug, Error)]
51pub enum EngineError {
52 #[error(
54 "Invalid engine: {0}. Expected 'chromiumoxide', 'fantoccini', 'playwright', or 'puppeteer'"
55 )]
56 InvalidEngine(String),
57
58 #[error("Element not found: {0}")]
60 ElementNotFound(String),
61
62 #[error("Operation timed out: {0}")]
64 Timeout(String),
65
66 #[error("Navigation error: {0}")]
68 Navigation(String),
69
70 #[error("JavaScript evaluation error: {0}")]
72 Evaluation(String),
73
74 #[error("Browser error: {0}")]
76 Browser(String),
77}
78
79#[derive(Debug, Clone)]
81pub struct ElementInfo {
82 pub tag_name: String,
84 pub text_content: Option<String>,
86 pub is_visible: bool,
88 pub is_enabled: bool,
90 pub bounding_box: Option<(f64, f64, f64, f64)>,
92}
93
94#[derive(Debug, Clone)]
96pub struct ClickVerificationResult {
97 pub verified: bool,
99 pub reason: String,
101 pub navigation_error: bool,
103}
104
105#[derive(Debug, Clone)]
107pub struct ScrollVerificationResult {
108 pub verified: bool,
110 pub in_viewport: bool,
112 pub attempts: u32,
114}
115
116#[derive(Debug, Clone)]
118pub struct FillVerificationResult {
119 pub verified: bool,
121 pub actual_value: String,
123 pub attempts: u32,
125}
126
127#[derive(Debug, Clone, Default)]
129pub struct PdfOptions {
130 pub format: Option<String>,
132 pub print_background: bool,
134 pub margin_top: Option<String>,
136 pub margin_right: Option<String>,
137 pub margin_bottom: Option<String>,
138 pub margin_left: Option<String>,
139 pub scale: Option<f64>,
141 pub path: Option<String>,
143}
144
145#[derive(Debug, Clone, Default)]
147pub struct PreClickState {
148 pub disabled: Option<bool>,
150 pub aria_pressed: Option<String>,
152 pub aria_expanded: Option<String>,
154 pub aria_selected: Option<String>,
156 pub checked: Option<bool>,
158 pub class_name: Option<String>,
160 pub is_connected: bool,
162}
163
164#[async_trait]
169pub trait EngineAdapter: Send + Sync {
170 fn engine_type(&self) -> EngineType;
172
173 async fn url(&self) -> Result<String, EngineError>;
175
176 async fn goto(&self, url: &str) -> Result<(), EngineError>;
178
179 async fn query_selector(&self, selector: &str) -> Result<Option<ElementInfo>, EngineError>;
181
182 async fn query_selector_all(&self, selector: &str) -> Result<Vec<ElementInfo>, EngineError>;
184
185 async fn count(&self, selector: &str) -> Result<usize, EngineError>;
187
188 async fn click(&self, selector: &str) -> Result<(), EngineError>;
190
191 async fn fill(&self, selector: &str, text: &str) -> Result<(), EngineError>;
193
194 async fn type_text(&self, selector: &str, text: &str) -> Result<(), EngineError>;
196
197 async fn text_content(&self, selector: &str) -> Result<Option<String>, EngineError>;
199
200 async fn input_value(&self, selector: &str) -> Result<Option<String>, EngineError>;
202
203 async fn get_attribute(
205 &self,
206 selector: &str,
207 attribute: &str,
208 ) -> Result<Option<String>, EngineError>;
209
210 async fn is_visible(&self, selector: &str) -> Result<bool, EngineError>;
212
213 async fn is_enabled(&self, selector: &str) -> Result<bool, EngineError>;
215
216 async fn wait_for_selector(&self, selector: &str, timeout_ms: u64) -> Result<(), EngineError>;
218
219 async fn scroll_into_view(&self, selector: &str) -> Result<(), EngineError>;
221
222 async fn evaluate(&self, script: &str) -> Result<serde_json::Value, EngineError>;
224
225 async fn screenshot(&self) -> Result<Vec<u8>, EngineError>;
227
228 async fn pdf(&self, options: PdfOptions) -> Result<Vec<u8>, EngineError>;
233
234 async fn bring_to_front(&self) -> Result<(), EngineError>;
236
237 async fn wait_for_navigation(&self, timeout_ms: u64) -> Result<(), EngineError>;
239
240 async fn keyboard_press(&self, key: &str) -> Result<(), EngineError>;
248
249 async fn keyboard_type(&self, text: &str) -> Result<(), EngineError>;
251
252 async fn keyboard_down(&self, key: &str) -> Result<(), EngineError>;
254
255 async fn keyboard_up(&self, key: &str) -> Result<(), EngineError>;
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn engine_type_display() {
265 assert_eq!(EngineType::Chromiumoxide.to_string(), "chromiumoxide");
266 assert_eq!(EngineType::Fantoccini.to_string(), "fantoccini");
267 assert_eq!(EngineType::Playwright.to_string(), "playwright");
268 assert_eq!(EngineType::Puppeteer.to_string(), "puppeteer");
269 }
270
271 #[test]
272 fn engine_type_from_str() {
273 assert_eq!(
274 "chromiumoxide".parse::<EngineType>().unwrap(),
275 EngineType::Chromiumoxide
276 );
277 assert_eq!(
278 "cdp".parse::<EngineType>().unwrap(),
279 EngineType::Chromiumoxide
280 );
281 assert_eq!(
282 "puppeteer".parse::<EngineType>().unwrap(),
283 EngineType::Puppeteer
284 );
285 assert_eq!(
286 "puppeteer".parse::<EngineType>().unwrap().to_string(),
287 "puppeteer"
288 );
289 assert_eq!(
290 "fantoccini".parse::<EngineType>().unwrap(),
291 EngineType::Fantoccini
292 );
293 assert_eq!(
294 "webdriver".parse::<EngineType>().unwrap(),
295 EngineType::Fantoccini
296 );
297 assert_eq!(
298 "playwright".parse::<EngineType>().unwrap(),
299 EngineType::Playwright
300 );
301 assert_eq!(
302 "playwright".parse::<EngineType>().unwrap().to_string(),
303 "playwright"
304 );
305 }
306
307 #[test]
308 fn engine_type_from_str_case_insensitive() {
309 assert_eq!(
310 "CHROMIUMOXIDE".parse::<EngineType>().unwrap(),
311 EngineType::Chromiumoxide
312 );
313 assert_eq!(
314 "Fantoccini".parse::<EngineType>().unwrap(),
315 EngineType::Fantoccini
316 );
317 assert_eq!(
318 "Playwright".parse::<EngineType>().unwrap(),
319 EngineType::Playwright
320 );
321 assert_eq!(
322 "Puppeteer".parse::<EngineType>().unwrap(),
323 EngineType::Puppeteer
324 );
325 }
326
327 #[test]
328 fn engine_type_from_str_invalid() {
329 let result = "invalid".parse::<EngineType>();
330 assert!(result.is_err());
331 if let Err(EngineError::InvalidEngine(name)) = result {
332 assert_eq!(name, "invalid");
333 } else {
334 panic!("Expected InvalidEngine error");
335 }
336 }
337
338 #[test]
339 fn pdf_options_default() {
340 let opts = PdfOptions::default();
341 assert!(opts.format.is_none());
342 assert!(!opts.print_background);
343 assert!(opts.margin_top.is_none());
344 assert!(opts.path.is_none());
345 assert!(opts.scale.is_none());
346 }
347
348 #[test]
349 fn pdf_options_can_be_constructed() {
350 let opts = PdfOptions {
351 format: Some("A4".to_string()),
352 print_background: true,
353 margin_top: Some("1cm".to_string()),
354 margin_right: Some("1cm".to_string()),
355 margin_bottom: Some("1cm".to_string()),
356 margin_left: Some("1cm".to_string()),
357 scale: Some(1.0),
358 path: None,
359 };
360 assert_eq!(opts.format.as_deref(), Some("A4"));
361 assert!(opts.print_background);
362 assert_eq!(opts.margin_top.as_deref(), Some("1cm"));
363 assert_eq!(opts.scale, Some(1.0));
364 }
365
366 #[test]
367 fn pre_click_state_default() {
368 let state = PreClickState::default();
369 assert!(state.disabled.is_none());
370 assert!(state.aria_pressed.is_none());
371 assert!(!state.is_connected);
372 }
373
374 #[test]
375 fn click_verification_result_creation() {
376 let result = ClickVerificationResult {
377 verified: true,
378 reason: "element state changed".to_string(),
379 navigation_error: false,
380 };
381 assert!(result.verified);
382 assert!(!result.navigation_error);
383 }
384}