1use crate::error::BrowserError;
7use async_trait::async_trait;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::collections::HashMap;
11use std::sync::Mutex;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct TabInfo {
16 pub id: String,
18 pub url: String,
20 pub title: String,
22 pub active: bool,
24}
25
26#[async_trait]
31pub trait CdpClient: Send + Sync {
32 async fn navigate(&self, url: &str) -> Result<(), BrowserError>;
34
35 async fn go_back(&self) -> Result<(), BrowserError>;
37
38 async fn go_forward(&self) -> Result<(), BrowserError>;
40
41 async fn refresh(&self) -> Result<(), BrowserError>;
43
44 async fn click(&self, selector: &str) -> Result<(), BrowserError>;
46
47 async fn type_text(&self, selector: &str, text: &str) -> Result<(), BrowserError>;
49
50 async fn fill(&self, selector: &str, value: &str) -> Result<(), BrowserError>;
52
53 async fn select_option(&self, selector: &str, value: &str) -> Result<(), BrowserError>;
55
56 async fn hover(&self, selector: &str) -> Result<(), BrowserError>;
58
59 async fn press_key(&self, key: &str) -> Result<(), BrowserError>;
61
62 async fn scroll(&self, x: i32, y: i32) -> Result<(), BrowserError>;
64
65 async fn screenshot(&self) -> Result<Vec<u8>, BrowserError>;
67
68 async fn get_html(&self) -> Result<String, BrowserError>;
70
71 async fn get_text(&self) -> Result<String, BrowserError>;
73
74 async fn get_url(&self) -> Result<String, BrowserError>;
76
77 async fn get_title(&self) -> Result<String, BrowserError>;
79
80 async fn evaluate_js(&self, script: &str) -> Result<Value, BrowserError>;
82
83 async fn wait_for_selector(&self, selector: &str, timeout_ms: u64) -> Result<(), BrowserError>;
85
86 async fn get_aria_tree(&self) -> Result<String, BrowserError>;
88
89 async fn upload_file(&self, selector: &str, path: &str) -> Result<(), BrowserError>;
91
92 async fn close(&self) -> Result<(), BrowserError>;
94
95 async fn new_tab(&self, url: &str) -> Result<String, BrowserError>;
99
100 async fn list_tabs(&self) -> Result<Vec<TabInfo>, BrowserError>;
102
103 async fn switch_tab(&self, tab_id: &str) -> Result<(), BrowserError>;
105
106 async fn close_tab(&self, tab_id: &str) -> Result<(), BrowserError>;
108
109 async fn active_tab_id(&self) -> Result<String, BrowserError>;
111}
112
113pub struct MockCdpClient {
115 pub current_url: Mutex<String>,
117 pub current_title: Mutex<String>,
119 pub html_content: Mutex<String>,
121 pub text_content: Mutex<String>,
123 pub aria_tree: Mutex<String>,
125 pub screenshot_bytes: Mutex<Vec<u8>>,
127 pub js_results: Mutex<HashMap<String, Value>>,
129 pub call_log: Mutex<Vec<(String, Vec<String>)>>,
131 pub navigate_error: Mutex<Option<BrowserError>>,
133 pub click_error: Mutex<Option<BrowserError>>,
135 pub wait_error: Mutex<Option<BrowserError>>,
137 pub closed: Mutex<bool>,
139 pub tabs: Mutex<Vec<TabInfo>>,
141 pub active_tab: Mutex<String>,
143 tab_counter: Mutex<u32>,
145}
146
147impl Default for MockCdpClient {
148 fn default() -> Self {
149 let default_tab_id = "tab-0".to_string();
150 Self {
151 current_url: Mutex::new("about:blank".to_string()),
152 current_title: Mutex::new(String::new()),
153 html_content: Mutex::new("<html><body></body></html>".to_string()),
154 text_content: Mutex::new(String::new()),
155 aria_tree: Mutex::new("document\n body".to_string()),
156 screenshot_bytes: Mutex::new(vec![0x89, 0x50, 0x4E, 0x47]), js_results: Mutex::new(HashMap::new()),
158 call_log: Mutex::new(Vec::new()),
159 navigate_error: Mutex::new(None),
160 click_error: Mutex::new(None),
161 wait_error: Mutex::new(None),
162 closed: Mutex::new(false),
163 tabs: Mutex::new(vec![TabInfo {
164 id: default_tab_id.clone(),
165 url: "about:blank".to_string(),
166 title: String::new(),
167 active: true,
168 }]),
169 active_tab: Mutex::new(default_tab_id),
170 tab_counter: Mutex::new(1),
171 }
172 }
173}
174
175impl MockCdpClient {
176 pub fn new() -> Self {
177 Self::default()
178 }
179
180 pub fn set_url(&self, url: impl Into<String>) {
182 *self.current_url.lock().unwrap() = url.into();
183 }
184
185 pub fn set_title(&self, title: impl Into<String>) {
187 *self.current_title.lock().unwrap() = title.into();
188 }
189
190 pub fn set_html(&self, html: impl Into<String>) {
192 *self.html_content.lock().unwrap() = html.into();
193 }
194
195 pub fn set_text(&self, text: impl Into<String>) {
197 *self.text_content.lock().unwrap() = text.into();
198 }
199
200 pub fn set_aria_tree(&self, tree: impl Into<String>) {
202 *self.aria_tree.lock().unwrap() = tree.into();
203 }
204
205 pub fn set_screenshot(&self, bytes: Vec<u8>) {
207 *self.screenshot_bytes.lock().unwrap() = bytes;
208 }
209
210 pub fn add_js_result(&self, script: impl Into<String>, result: Value) {
212 self.js_results
213 .lock()
214 .unwrap()
215 .insert(script.into(), result);
216 }
217
218 pub fn set_navigate_error(&self, err: BrowserError) {
220 *self.navigate_error.lock().unwrap() = Some(err);
221 }
222
223 pub fn set_click_error(&self, err: BrowserError) {
225 *self.click_error.lock().unwrap() = Some(err);
226 }
227
228 pub fn set_wait_error(&self, err: BrowserError) {
230 *self.wait_error.lock().unwrap() = Some(err);
231 }
232
233 fn log_call(&self, method: &str, args: Vec<String>) {
234 self.call_log
235 .lock()
236 .unwrap()
237 .push((method.to_string(), args));
238 }
239
240 pub fn call_count(&self, method: &str) -> usize {
242 self.call_log
243 .lock()
244 .unwrap()
245 .iter()
246 .filter(|(m, _)| m == method)
247 .count()
248 }
249
250 pub fn calls(&self) -> Vec<(String, Vec<String>)> {
252 self.call_log.lock().unwrap().clone()
253 }
254}
255
256#[async_trait]
257impl CdpClient for MockCdpClient {
258 async fn navigate(&self, url: &str) -> Result<(), BrowserError> {
259 self.log_call("navigate", vec![url.to_string()]);
260 if let Some(err) = self.navigate_error.lock().unwrap().take() {
261 return Err(err);
262 }
263 *self.current_url.lock().unwrap() = url.to_string();
264 Ok(())
265 }
266
267 async fn go_back(&self) -> Result<(), BrowserError> {
268 self.log_call("go_back", vec![]);
269 Ok(())
270 }
271
272 async fn go_forward(&self) -> Result<(), BrowserError> {
273 self.log_call("go_forward", vec![]);
274 Ok(())
275 }
276
277 async fn refresh(&self) -> Result<(), BrowserError> {
278 self.log_call("refresh", vec![]);
279 Ok(())
280 }
281
282 async fn click(&self, selector: &str) -> Result<(), BrowserError> {
283 self.log_call("click", vec![selector.to_string()]);
284 if let Some(err) = self.click_error.lock().unwrap().take() {
285 return Err(err);
286 }
287 Ok(())
288 }
289
290 async fn type_text(&self, selector: &str, text: &str) -> Result<(), BrowserError> {
291 self.log_call("type_text", vec![selector.to_string(), text.to_string()]);
292 Ok(())
293 }
294
295 async fn fill(&self, selector: &str, value: &str) -> Result<(), BrowserError> {
296 self.log_call("fill", vec![selector.to_string(), value.to_string()]);
297 Ok(())
298 }
299
300 async fn select_option(&self, selector: &str, value: &str) -> Result<(), BrowserError> {
301 self.log_call(
302 "select_option",
303 vec![selector.to_string(), value.to_string()],
304 );
305 Ok(())
306 }
307
308 async fn hover(&self, selector: &str) -> Result<(), BrowserError> {
309 self.log_call("hover", vec![selector.to_string()]);
310 Ok(())
311 }
312
313 async fn press_key(&self, key: &str) -> Result<(), BrowserError> {
314 self.log_call("press_key", vec![key.to_string()]);
315 Ok(())
316 }
317
318 async fn scroll(&self, x: i32, y: i32) -> Result<(), BrowserError> {
319 self.log_call("scroll", vec![x.to_string(), y.to_string()]);
320 Ok(())
321 }
322
323 async fn screenshot(&self) -> Result<Vec<u8>, BrowserError> {
324 self.log_call("screenshot", vec![]);
325 Ok(self.screenshot_bytes.lock().unwrap().clone())
326 }
327
328 async fn get_html(&self) -> Result<String, BrowserError> {
329 self.log_call("get_html", vec![]);
330 Ok(self.html_content.lock().unwrap().clone())
331 }
332
333 async fn get_text(&self) -> Result<String, BrowserError> {
334 self.log_call("get_text", vec![]);
335 Ok(self.text_content.lock().unwrap().clone())
336 }
337
338 async fn get_url(&self) -> Result<String, BrowserError> {
339 self.log_call("get_url", vec![]);
340 Ok(self.current_url.lock().unwrap().clone())
341 }
342
343 async fn get_title(&self) -> Result<String, BrowserError> {
344 self.log_call("get_title", vec![]);
345 Ok(self.current_title.lock().unwrap().clone())
346 }
347
348 async fn evaluate_js(&self, script: &str) -> Result<Value, BrowserError> {
349 self.log_call("evaluate_js", vec![script.to_string()]);
350 let results = self.js_results.lock().unwrap();
351 match results.get(script) {
352 Some(val) => Ok(val.clone()),
353 None => Ok(Value::Null),
354 }
355 }
356
357 async fn wait_for_selector(&self, selector: &str, timeout_ms: u64) -> Result<(), BrowserError> {
358 self.log_call(
359 "wait_for_selector",
360 vec![selector.to_string(), timeout_ms.to_string()],
361 );
362 if let Some(err) = self.wait_error.lock().unwrap().take() {
363 return Err(err);
364 }
365 Ok(())
366 }
367
368 async fn get_aria_tree(&self) -> Result<String, BrowserError> {
369 self.log_call("get_aria_tree", vec![]);
370 Ok(self.aria_tree.lock().unwrap().clone())
371 }
372
373 async fn upload_file(&self, selector: &str, path: &str) -> Result<(), BrowserError> {
374 self.log_call("upload_file", vec![selector.to_string(), path.to_string()]);
375 Ok(())
376 }
377
378 async fn close(&self) -> Result<(), BrowserError> {
379 self.log_call("close", vec![]);
380 *self.closed.lock().unwrap() = true;
381 Ok(())
382 }
383
384 async fn new_tab(&self, url: &str) -> Result<String, BrowserError> {
385 self.log_call("new_tab", vec![url.to_string()]);
386 let mut counter = self.tab_counter.lock().unwrap();
387 let tab_id = format!("tab-{}", *counter);
388 *counter += 1;
389 drop(counter);
390
391 let tab = TabInfo {
392 id: tab_id.clone(),
393 url: url.to_string(),
394 title: String::new(),
395 active: false,
396 };
397 self.tabs.lock().unwrap().push(tab);
398 Ok(tab_id)
399 }
400
401 async fn list_tabs(&self) -> Result<Vec<TabInfo>, BrowserError> {
402 self.log_call("list_tabs", vec![]);
403 let active = self.active_tab.lock().unwrap().clone();
404 let mut tabs = self.tabs.lock().unwrap().clone();
405 for tab in &mut tabs {
406 tab.active = tab.id == active;
407 }
408 Ok(tabs)
409 }
410
411 async fn switch_tab(&self, tab_id: &str) -> Result<(), BrowserError> {
412 self.log_call("switch_tab", vec![tab_id.to_string()]);
413 let tabs = self.tabs.lock().unwrap();
414 if !tabs.iter().any(|t| t.id == tab_id) {
415 return Err(BrowserError::TabNotFound {
416 tab_id: tab_id.to_string(),
417 });
418 }
419 drop(tabs);
420 *self.active_tab.lock().unwrap() = tab_id.to_string();
421 Ok(())
422 }
423
424 async fn close_tab(&self, tab_id: &str) -> Result<(), BrowserError> {
425 self.log_call("close_tab", vec![tab_id.to_string()]);
426 let mut tabs = self.tabs.lock().unwrap();
427 let initial_len = tabs.len();
428 tabs.retain(|t| t.id != tab_id);
429 if tabs.len() == initial_len {
430 return Err(BrowserError::TabNotFound {
431 tab_id: tab_id.to_string(),
432 });
433 }
434 Ok(())
435 }
436
437 async fn active_tab_id(&self) -> Result<String, BrowserError> {
438 self.log_call("active_tab_id", vec![]);
439 Ok(self.active_tab.lock().unwrap().clone())
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[tokio::test]
448 async fn test_mock_navigate() {
449 let client = MockCdpClient::new();
450 client.navigate("https://example.com").await.unwrap();
451 assert_eq!(*client.current_url.lock().unwrap(), "https://example.com");
452 assert_eq!(client.call_count("navigate"), 1);
453 }
454
455 #[tokio::test]
456 async fn test_mock_navigate_error() {
457 let client = MockCdpClient::new();
458 client.set_navigate_error(BrowserError::NavigationFailed {
459 message: "timeout".to_string(),
460 });
461 let result = client.navigate("https://example.com").await;
462 assert!(result.is_err());
463 }
464
465 #[tokio::test]
466 async fn test_mock_click() {
467 let client = MockCdpClient::new();
468 client.click("#submit").await.unwrap();
469 assert_eq!(client.call_count("click"), 1);
470 let calls = client.calls();
471 assert_eq!(calls[0].1[0], "#submit");
472 }
473
474 #[tokio::test]
475 async fn test_mock_click_error() {
476 let client = MockCdpClient::new();
477 client.set_click_error(BrowserError::ElementNotFound {
478 selector: "#missing".to_string(),
479 });
480 let result = client.click("#missing").await;
481 assert!(result.is_err());
482 }
483
484 #[tokio::test]
485 async fn test_mock_type_text() {
486 let client = MockCdpClient::new();
487 client.type_text("#input", "hello").await.unwrap();
488 assert_eq!(client.call_count("type_text"), 1);
489 }
490
491 #[tokio::test]
492 async fn test_mock_fill() {
493 let client = MockCdpClient::new();
494 client.fill("#email", "user@example.com").await.unwrap();
495 let calls = client.calls();
496 assert_eq!(calls[0].0, "fill");
497 assert_eq!(calls[0].1[1], "user@example.com");
498 }
499
500 #[tokio::test]
501 async fn test_mock_screenshot() {
502 let client = MockCdpClient::new();
503 client.set_screenshot(vec![1, 2, 3, 4]);
504 let bytes = client.screenshot().await.unwrap();
505 assert_eq!(bytes, vec![1, 2, 3, 4]);
506 }
507
508 #[tokio::test]
509 async fn test_mock_get_html() {
510 let client = MockCdpClient::new();
511 client.set_html("<html><body>Test</body></html>");
512 let html = client.get_html().await.unwrap();
513 assert_eq!(html, "<html><body>Test</body></html>");
514 }
515
516 #[tokio::test]
517 async fn test_mock_get_text() {
518 let client = MockCdpClient::new();
519 client.set_text("Hello World");
520 let text = client.get_text().await.unwrap();
521 assert_eq!(text, "Hello World");
522 }
523
524 #[tokio::test]
525 async fn test_mock_get_url_and_title() {
526 let client = MockCdpClient::new();
527 client.set_url("https://docs.rs");
528 client.set_title("Docs.rs");
529 assert_eq!(client.get_url().await.unwrap(), "https://docs.rs");
530 assert_eq!(client.get_title().await.unwrap(), "Docs.rs");
531 }
532
533 #[tokio::test]
534 async fn test_mock_evaluate_js() {
535 let client = MockCdpClient::new();
536 client.add_js_result("1+1", serde_json::json!(2));
537 let result = client.evaluate_js("1+1").await.unwrap();
538 assert_eq!(result, serde_json::json!(2));
539 }
540
541 #[tokio::test]
542 async fn test_mock_evaluate_js_unknown_script() {
543 let client = MockCdpClient::new();
544 let result = client.evaluate_js("unknown()").await.unwrap();
545 assert_eq!(result, Value::Null);
546 }
547
548 #[tokio::test]
549 async fn test_mock_wait_for_selector() {
550 let client = MockCdpClient::new();
551 client.wait_for_selector("#loaded", 5000).await.unwrap();
552 assert_eq!(client.call_count("wait_for_selector"), 1);
553 }
554
555 #[tokio::test]
556 async fn test_mock_wait_for_selector_timeout() {
557 let client = MockCdpClient::new();
558 client.set_wait_error(BrowserError::Timeout { timeout_secs: 5 });
559 let result = client.wait_for_selector("#never", 5000).await;
560 assert!(result.is_err());
561 }
562
563 #[tokio::test]
564 async fn test_mock_aria_tree() {
565 let client = MockCdpClient::new();
566 client.set_aria_tree("document\n heading 'Title'");
567 let tree = client.get_aria_tree().await.unwrap();
568 assert!(tree.contains("heading"));
569 }
570
571 #[tokio::test]
572 async fn test_mock_close() {
573 let client = MockCdpClient::new();
574 assert!(!*client.closed.lock().unwrap());
575 client.close().await.unwrap();
576 assert!(*client.closed.lock().unwrap());
577 }
578
579 #[tokio::test]
580 async fn test_mock_navigation_methods() {
581 let client = MockCdpClient::new();
582 client.go_back().await.unwrap();
583 client.go_forward().await.unwrap();
584 client.refresh().await.unwrap();
585 assert_eq!(client.call_count("go_back"), 1);
586 assert_eq!(client.call_count("go_forward"), 1);
587 assert_eq!(client.call_count("refresh"), 1);
588 }
589
590 #[tokio::test]
591 async fn test_mock_scroll() {
592 let client = MockCdpClient::new();
593 client.scroll(0, 500).await.unwrap();
594 let calls = client.calls();
595 assert_eq!(calls[0].0, "scroll");
596 assert_eq!(calls[0].1, vec!["0", "500"]);
597 }
598
599 #[tokio::test]
600 async fn test_mock_hover_and_press_key() {
601 let client = MockCdpClient::new();
602 client.hover("#menu").await.unwrap();
603 client.press_key("Enter").await.unwrap();
604 assert_eq!(client.call_count("hover"), 1);
605 assert_eq!(client.call_count("press_key"), 1);
606 }
607
608 #[tokio::test]
609 async fn test_mock_select_option() {
610 let client = MockCdpClient::new();
611 client.select_option("#country", "US").await.unwrap();
612 assert_eq!(client.call_count("select_option"), 1);
613 }
614
615 #[tokio::test]
616 async fn test_mock_upload_file() {
617 let client = MockCdpClient::new();
618 client
619 .upload_file("#file-input", "/tmp/test.txt")
620 .await
621 .unwrap();
622 let calls = client.calls();
623 assert_eq!(calls[0].0, "upload_file");
624 assert_eq!(calls[0].1[1], "/tmp/test.txt");
625 }
626
627 #[tokio::test]
630 async fn test_mock_default_has_one_tab() {
631 let client = MockCdpClient::new();
632 let tabs = client.list_tabs().await.unwrap();
633 assert_eq!(tabs.len(), 1);
634 assert_eq!(tabs[0].id, "tab-0");
635 assert!(tabs[0].active);
636 }
637
638 #[tokio::test]
639 async fn test_mock_new_tab() {
640 let client = MockCdpClient::new();
641 let tab_id = client.new_tab("https://example.com").await.unwrap();
642 assert_eq!(tab_id, "tab-1");
643 let tabs = client.list_tabs().await.unwrap();
644 assert_eq!(tabs.len(), 2);
645 assert_eq!(tabs[1].url, "https://example.com");
646 }
647
648 #[tokio::test]
649 async fn test_mock_switch_tab() {
650 let client = MockCdpClient::new();
651 let tab_id = client.new_tab("https://example.com").await.unwrap();
652 client.switch_tab(&tab_id).await.unwrap();
653 assert_eq!(client.active_tab_id().await.unwrap(), tab_id);
654 let tabs = client.list_tabs().await.unwrap();
656 assert!(!tabs[0].active);
657 assert!(tabs[1].active);
658 }
659
660 #[tokio::test]
661 async fn test_mock_switch_tab_not_found() {
662 let client = MockCdpClient::new();
663 let result = client.switch_tab("nonexistent").await;
664 assert!(result.is_err());
665 }
666
667 #[tokio::test]
668 async fn test_mock_close_tab() {
669 let client = MockCdpClient::new();
670 let tab_id = client.new_tab("https://example.com").await.unwrap();
671 assert_eq!(client.list_tabs().await.unwrap().len(), 2);
672 client.close_tab(&tab_id).await.unwrap();
673 assert_eq!(client.list_tabs().await.unwrap().len(), 1);
674 }
675
676 #[tokio::test]
677 async fn test_mock_close_tab_not_found() {
678 let client = MockCdpClient::new();
679 let result = client.close_tab("nonexistent").await;
680 assert!(result.is_err());
681 }
682
683 #[tokio::test]
684 async fn test_mock_active_tab_id() {
685 let client = MockCdpClient::new();
686 assert_eq!(client.active_tab_id().await.unwrap(), "tab-0");
687 }
688
689 #[tokio::test]
690 async fn test_mock_multiple_tabs() {
691 let client = MockCdpClient::new();
692 let t1 = client.new_tab("https://one.com").await.unwrap();
693 let t2 = client.new_tab("https://two.com").await.unwrap();
694 let _t3 = client.new_tab("https://three.com").await.unwrap();
695 assert_eq!(client.list_tabs().await.unwrap().len(), 4); client.close_tab(&t1).await.unwrap();
697 assert_eq!(client.list_tabs().await.unwrap().len(), 3);
698 client.switch_tab(&t2).await.unwrap();
699 assert_eq!(client.active_tab_id().await.unwrap(), t2);
700 }
701}