1#![allow(
16 clippy::cast_possible_truncation,
17 clippy::cast_sign_loss,
18 clippy::cast_precision_loss
19)]
20
21use chromiumoxide::Page;
22use chromiumoxide::cdp::browser_protocol::input::{DispatchKeyEventParams, DispatchKeyEventType};
23use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
24use tokio::time::sleep;
25use tracing::warn;
26
27use crate::error::{BrowserError, Result};
28
29const fn splitmix64(state: &mut u64) -> u64 {
33 *state = state.wrapping_add(0x9e37_79b9_7f4a_7c15);
34 let mut z = *state;
35 z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
36 z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
37 z ^ (z >> 31)
38}
39
40fn rand_f64(state: &mut u64) -> f64 {
42 (splitmix64(state) >> 11) as f64 / (1u64 << 53) as f64
43}
44
45fn rand_range(state: &mut u64, min: f64, max: f64) -> f64 {
47 rand_f64(state).mul_add(max - min, min)
48}
49
50fn rand_normal(state: &mut u64, mean: f64, std_dev: f64) -> f64 {
52 let u1 = rand_f64(state).max(1e-10);
53 let u2 = rand_f64(state);
54 let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
55 std_dev.mul_add(z, mean)
56}
57
58fn lerp(p0: (f64, f64), p1: (f64, f64), t: f64) -> (f64, f64) {
61 (t.mul_add(p1.0 - p0.0, p0.0), t.mul_add(p1.1 - p0.1, p0.1))
62}
63
64fn cubic_bezier(
66 p0: (f64, f64),
67 p1: (f64, f64),
68 p2: (f64, f64),
69 p3: (f64, f64),
70 t: f64,
71) -> (f64, f64) {
72 let a = lerp(p0, p1, t);
73 let b = lerp(p1, p2, t);
74 let c = lerp(p2, p3, t);
75 lerp(lerp(a, b, t), lerp(b, c, t), t)
76}
77
78pub struct MouseSimulator {
101 current_x: f64,
103 current_y: f64,
105 rng: u64,
107}
108
109impl Default for MouseSimulator {
110 fn default() -> Self {
111 Self::new()
112 }
113}
114
115impl MouseSimulator {
116 pub fn new() -> Self {
126 let seed = SystemTime::now()
127 .duration_since(UNIX_EPOCH)
128 .map(|d| d.as_secs() ^ u64::from(d.subsec_nanos()))
129 .unwrap_or(0x1234_5678_9abc_def0);
130 Self {
131 current_x: 0.0,
132 current_y: 0.0,
133 rng: seed,
134 }
135 }
136
137 pub const fn with_seed_and_position(seed: u64, x: f64, y: f64) -> Self {
149 Self {
150 current_x: x,
151 current_y: y,
152 rng: seed,
153 }
154 }
155
156 pub const fn position(&self) -> (f64, f64) {
167 (self.current_x, self.current_y)
168 }
169
170 pub fn compute_path(
196 &mut self,
197 from_x: f64,
198 from_y: f64,
199 to_x: f64,
200 to_y: f64,
201 ) -> Vec<(f64, f64)> {
202 let dx = to_x - from_x;
203 let dy = to_y - from_y;
204 let distance = dx.hypot(dy);
205
206 let steps = ((distance / 8.0).round() as usize).clamp(12, 120);
208
209 let (px, py) = if distance > 1.0 {
211 (-dy / distance, dx / distance)
212 } else {
213 (1.0, 0.0)
214 };
215
216 let offset_scale = (distance * 0.35).min(200.0);
218 let cp1_off = rand_normal(&mut self.rng, 0.0, offset_scale * 0.5);
219 let cp2_off = rand_normal(&mut self.rng, 0.0, offset_scale * 0.4);
220
221 let cp1 = (
223 px.mul_add(cp1_off, from_x + dx / 3.0),
224 py.mul_add(cp1_off, from_y + dy / 3.0),
225 );
226 let cp2 = (
227 px.mul_add(cp2_off, from_x + 2.0 * dx / 3.0),
228 py.mul_add(cp2_off, from_y + 2.0 * dy / 3.0),
229 );
230 let p0 = (from_x, from_y);
231 let p3 = (to_x, to_y);
232
233 (0..=steps)
234 .map(|i| {
235 let t = i as f64 / steps as f64;
236 let (bx, by) = cubic_bezier(p0, cp1, cp2, p3, t);
237 let jx = rand_normal(&mut self.rng, 0.0, 0.4);
239 let jy = rand_normal(&mut self.rng, 0.0, 0.4);
240 (bx + jx, by + jy)
241 })
242 .collect()
243 }
244
245 pub async fn move_to(&mut self, page: &Page, to_x: f64, to_y: f64) -> Result<()> {
255 use chromiumoxide::cdp::browser_protocol::input::{
256 DispatchMouseEventParams, DispatchMouseEventType,
257 };
258
259 let path = self.compute_path(self.current_x, self.current_y, to_x, to_y);
260
261 for &(x, y) in &path {
262 let params = DispatchMouseEventParams::builder()
263 .r#type(DispatchMouseEventType::MouseMoved)
264 .x(x)
265 .y(y)
266 .build()
267 .map_err(BrowserError::ConfigError)?;
268
269 page.execute(params)
270 .await
271 .map_err(|e| BrowserError::CdpError {
272 operation: "Input.dispatchMouseEvent(mouseMoved)".to_string(),
273 message: e.to_string(),
274 })?;
275
276 let delay_ms = rand_range(&mut self.rng, 10.0, 50.0) as u64;
277 sleep(Duration::from_millis(delay_ms)).await;
278 }
279
280 self.current_x = to_x;
281 self.current_y = to_y;
282 Ok(())
283 }
284
285 pub async fn click(&mut self, page: &Page, x: f64, y: f64) -> Result<()> {
294 use chromiumoxide::cdp::browser_protocol::input::{
295 DispatchMouseEventParams, DispatchMouseEventType, MouseButton,
296 };
297
298 self.move_to(page, x, y).await?;
299
300 let pre_ms = rand_range(&mut self.rng, 20.0, 80.0) as u64;
302 sleep(Duration::from_millis(pre_ms)).await;
303
304 let press = DispatchMouseEventParams::builder()
305 .r#type(DispatchMouseEventType::MousePressed)
306 .x(x)
307 .y(y)
308 .button(MouseButton::Left)
309 .click_count(1i64)
310 .build()
311 .map_err(BrowserError::ConfigError)?;
312
313 page.execute(press)
314 .await
315 .map_err(|e| BrowserError::CdpError {
316 operation: "Input.dispatchMouseEvent(mousePressed)".to_string(),
317 message: e.to_string(),
318 })?;
319
320 let hold_ms = rand_range(&mut self.rng, 50.0, 150.0) as u64;
322 sleep(Duration::from_millis(hold_ms)).await;
323
324 let release = DispatchMouseEventParams::builder()
325 .r#type(DispatchMouseEventType::MouseReleased)
326 .x(x)
327 .y(y)
328 .button(MouseButton::Left)
329 .click_count(1i64)
330 .build()
331 .map_err(BrowserError::ConfigError)?;
332
333 page.execute(release)
334 .await
335 .map_err(|e| BrowserError::CdpError {
336 operation: "Input.dispatchMouseEvent(mouseReleased)".to_string(),
337 message: e.to_string(),
338 })?;
339
340 Ok(())
341 }
342}
343
344fn adjacent_key(ch: char, rng: &mut u64) -> char {
351 const ROWS: [&str; 3] = ["qwertyuiop", "asdfghjkl", "zxcvbnm"];
352 let lc = ch.to_lowercase().next().unwrap_or(ch);
353 for row in ROWS {
354 let chars: Vec<char> = row.chars().collect();
355 if let Some(idx) = chars.iter().position(|&c| c == lc) {
356 let adj = if idx == 0 {
357 chars.get(1).copied().unwrap_or(lc)
358 } else if idx == chars.len() - 1 || rand_f64(rng) < 0.5 {
359 chars.get(idx - 1).copied().unwrap_or(lc)
360 } else {
361 chars.get(idx + 1).copied().unwrap_or(lc)
362 };
363 return if ch.is_uppercase() {
364 adj.to_uppercase().next().unwrap_or(adj)
365 } else {
366 adj
367 };
368 }
369 }
370 'x'
371}
372
373pub struct TypingSimulator {
395 rng: u64,
397 error_rate: f64,
399}
400
401impl Default for TypingSimulator {
402 fn default() -> Self {
403 Self::new()
404 }
405}
406
407impl TypingSimulator {
408 pub fn new() -> Self {
417 let seed = SystemTime::now()
418 .duration_since(UNIX_EPOCH)
419 .map(|d| d.as_secs() ^ u64::from(d.subsec_nanos()))
420 .unwrap_or(0xdead_beef_cafe_babe);
421 Self {
422 rng: seed,
423 error_rate: 0.015,
424 }
425 }
426
427 pub const fn with_seed(seed: u64) -> Self {
436 Self {
437 rng: seed,
438 error_rate: 0.015,
439 }
440 }
441
442 #[must_use]
453 pub const fn with_error_rate(mut self, rate: f64) -> Self {
454 self.error_rate = rate.clamp(0.0, 1.0);
455 self
456 }
457
458 pub fn keystroke_delay(&mut self) -> Duration {
471 let ms = rand_normal(&mut self.rng, 80.0, 25.0).clamp(30.0, 200.0) as u64;
472 Duration::from_millis(ms)
473 }
474
475 async fn dispatch_key(
477 page: &Page,
478 kind: DispatchKeyEventType,
479 key: &str,
480 text: Option<&str>,
481 modifiers: i64,
482 ) -> Result<()> {
483 let mut b = DispatchKeyEventParams::builder().r#type(kind).key(key);
484 if let Some(t) = text {
485 b = b.text(t);
486 }
487 if modifiers != 0 {
488 b = b.modifiers(modifiers);
489 }
490 let params = b.build().map_err(BrowserError::ConfigError)?;
491 page.execute(params)
492 .await
493 .map_err(|e| BrowserError::CdpError {
494 operation: "Input.dispatchKeyEvent".to_string(),
495 message: e.to_string(),
496 })?;
497 Ok(())
498 }
499
500 async fn type_backspace(page: &Page) -> Result<()> {
502 Self::dispatch_key(page, DispatchKeyEventType::RawKeyDown, "Backspace", None, 0).await?;
503 Self::dispatch_key(page, DispatchKeyEventType::KeyUp, "Backspace", None, 0).await?;
504 Ok(())
505 }
506
507 async fn type_char(page: &Page, ch: char) -> Result<()> {
512 let text = ch.to_string();
513 let modifiers: i64 = if ch.is_uppercase() && ch.is_alphabetic() {
514 8
515 } else {
516 0
517 };
518 let key = text.as_str();
519 Self::dispatch_key(
520 page,
521 DispatchKeyEventType::KeyDown,
522 key,
523 Some(&text),
524 modifiers,
525 )
526 .await?;
527 Self::dispatch_key(
528 page,
529 DispatchKeyEventType::Char,
530 key,
531 Some(&text),
532 modifiers,
533 )
534 .await?;
535 Self::dispatch_key(page, DispatchKeyEventType::KeyUp, key, None, modifiers).await?;
536 Ok(())
537 }
538
539 pub async fn type_text(&mut self, page: &Page, text: &str) -> Result<()> {
550 for ch in text.chars() {
551 if rand_f64(&mut self.rng) < self.error_rate {
553 let wrong = adjacent_key(ch, &mut self.rng);
554 Self::type_char(page, wrong).await?;
555 let typo_delay = rand_normal(&mut self.rng, 120.0, 30.0).clamp(60.0, 250.0) as u64;
556 sleep(Duration::from_millis(typo_delay)).await;
557 Self::type_backspace(page).await?;
558 let fix_delay = rand_range(&mut self.rng, 40.0, 120.0) as u64;
559 sleep(Duration::from_millis(fix_delay)).await;
560 }
561
562 Self::type_char(page, ch).await?;
563 sleep(self.keystroke_delay()).await;
564
565 if ch == ' ' || ch == '\n' {
567 let word_pause = rand_range(&mut self.rng, 100.0, 400.0) as u64;
568 sleep(Duration::from_millis(word_pause)).await;
569 }
570 }
571 Ok(())
572 }
573}
574
575#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
586pub enum InteractionLevel {
587 #[default]
589 None,
590 Low,
592 Medium,
594 High,
596}
597
598pub struct InteractionSimulator {
617 rng: u64,
618 mouse: MouseSimulator,
619 level: InteractionLevel,
620}
621
622impl Default for InteractionSimulator {
623 fn default() -> Self {
624 Self::new(InteractionLevel::None)
625 }
626}
627
628impl InteractionSimulator {
629 pub fn new(level: InteractionLevel) -> Self {
638 let seed = SystemTime::now()
639 .duration_since(UNIX_EPOCH)
640 .map(|d| d.as_secs() ^ u64::from(d.subsec_nanos()))
641 .unwrap_or(0x0123_4567_89ab_cdef);
642 Self {
643 rng: seed,
644 mouse: MouseSimulator::with_seed_and_position(seed ^ 0xca11_ab1e, 400.0, 300.0),
645 level,
646 }
647 }
648
649 pub const fn with_seed(seed: u64, level: InteractionLevel) -> Self {
658 Self {
659 rng: seed,
660 mouse: MouseSimulator::with_seed_and_position(seed ^ 0xca11_ab1e, 400.0, 300.0),
661 level,
662 }
663 }
664
665 async fn js(page: &Page, expr: String) -> Result<()> {
667 page.evaluate(expr)
668 .await
669 .map_err(|e| BrowserError::CdpError {
670 operation: "Runtime.evaluate".to_string(),
671 message: e.to_string(),
672 })?;
673 Ok(())
674 }
675
676 async fn scroll(page: &Page, delta_y: i64) -> Result<()> {
678 Self::js(
679 page,
680 format!("window.scrollBy({{top:{delta_y},behavior:'smooth'}})"),
681 )
682 .await
683 }
684
685 async fn do_keyactivity(&mut self, page: &Page) -> Result<()> {
693 const KEYS: &[&str] = &["ArrowDown", "Tab", "ArrowRight", "ArrowUp"];
694 let count = 3 + rand_range(&mut self.rng, 0.0, 4.0) as u32;
695 let mut successful_pairs = 0u32;
696 for i in 0..count {
697 let key = KEYS
698 .get((i as usize) % KEYS.len())
699 .copied()
700 .unwrap_or("Tab");
701 let down_delay = rand_range(&mut self.rng, 50.0, 120.0) as u64;
702 sleep(Duration::from_millis(down_delay)).await;
703 let keydown_ok = if let Err(e) = Self::js(
704 page,
705 format!(
706 "window.dispatchEvent(new KeyboardEvent('keydown',\
707 {{bubbles:true,cancelable:true,key:{key:?},code:{key:?}}}));"
708 ),
709 )
710 .await
711 {
712 warn!(key, "Failed to dispatch keydown event: {e}");
713 false
714 } else {
715 true
716 };
717 let hold_ms = rand_range(&mut self.rng, 20.0, 60.0) as u64;
718 sleep(Duration::from_millis(hold_ms)).await;
719 let keyup_ok = if let Err(e) = Self::js(
720 page,
721 format!(
722 "window.dispatchEvent(new KeyboardEvent('keyup',\
723 {{bubbles:true,cancelable:true,key:{key:?},code:{key:?}}}));"
724 ),
725 )
726 .await
727 {
728 warn!(key, "Failed to dispatch keyup event: {e}");
729 false
730 } else {
731 true
732 };
733
734 if keydown_ok && keyup_ok {
735 successful_pairs += 1;
736 }
737 }
738
739 if successful_pairs == 0 {
740 return Err(BrowserError::CdpError {
741 operation: "InteractionSimulator::do_keyactivity".to_string(),
742 message: "all synthetic key event dispatches failed".to_string(),
743 });
744 }
745
746 Ok(())
747 }
748
749 async fn do_scroll(&mut self, page: &Page) -> Result<()> {
751 let down = rand_range(&mut self.rng, 200.0, 600.0) as i64;
752 Self::scroll(page, down).await?;
753 let pause = rand_range(&mut self.rng, 300.0, 1_000.0) as u64;
754 sleep(Duration::from_millis(pause)).await;
755 let up = -(rand_range(&mut self.rng, 50.0, (down as f64) * 0.4) as i64);
756 Self::scroll(page, up).await?;
757 Ok(())
758 }
759
760 async fn do_mouse_wiggle(&mut self, page: &Page, vw: f64, vh: f64) -> Result<()> {
762 let tx = rand_range(&mut self.rng, vw * 0.1, vw * 0.9);
763 let ty = rand_range(&mut self.rng, vh * 0.1, vh * 0.9);
764 self.mouse.move_to(page, tx, ty).await
765 }
766
767 pub async fn random_interaction(
785 &mut self,
786 page: &Page,
787 viewport_w: f64,
788 viewport_h: f64,
789 ) -> Result<()> {
790 match self.level {
791 InteractionLevel::None => {}
792 InteractionLevel::Low => {
793 self.do_scroll(page).await?;
794 let pause = rand_range(&mut self.rng, 500.0, 1_500.0) as u64;
795 sleep(Duration::from_millis(pause)).await;
796 }
797 InteractionLevel::Medium => {
798 self.do_scroll(page).await?;
799 let p1 = rand_range(&mut self.rng, 800.0, 2_000.0) as u64;
800 sleep(Duration::from_millis(p1)).await;
801 self.do_keyactivity(page).await?;
803 let p2 = rand_range(&mut self.rng, 500.0, 1_500.0) as u64;
804 sleep(Duration::from_millis(p2)).await;
805 self.do_mouse_wiggle(page, viewport_w, viewport_h).await?;
806 let p3 = rand_range(&mut self.rng, 400.0, 1_500.0) as u64;
807 sleep(Duration::from_millis(p3)).await;
808 }
809 InteractionLevel::High => {
810 self.do_scroll(page).await?;
811 let p1 = rand_range(&mut self.rng, 1_000.0, 5_000.0) as u64;
812 sleep(Duration::from_millis(p1)).await;
813 self.do_keyactivity(page).await?;
814 let p2 = rand_range(&mut self.rng, 400.0, 1_200.0) as u64;
815 sleep(Duration::from_millis(p2)).await;
816 self.do_mouse_wiggle(page, viewport_w, viewport_h).await?;
817 let p3 = rand_range(&mut self.rng, 800.0, 3_000.0) as u64;
818 sleep(Duration::from_millis(p3)).await;
819 self.do_keyactivity(page).await?;
820 let p4 = rand_range(&mut self.rng, 300.0, 800.0) as u64;
821 sleep(Duration::from_millis(p4)).await;
822 self.do_mouse_wiggle(page, viewport_w, viewport_h).await?;
823 let p5 = rand_range(&mut self.rng, 500.0, 2_000.0) as u64;
824 sleep(Duration::from_millis(p5)).await;
825 if rand_f64(&mut self.rng) < 0.4 {
827 let up = -(rand_range(&mut self.rng, 50.0, 200.0) as i64);
828 Self::scroll(page, up).await?;
829 sleep(Duration::from_millis(500)).await;
830 }
831 }
832 }
833 Ok(())
834 }
835}
836
837pub struct RequestPacer {
863 rng: u64,
864 mean_ms: u64,
865 std_ms: u64,
866 min_ms: u64,
867 max_ms: u64,
868 last_request: Option<Instant>,
869}
870
871impl Default for RequestPacer {
872 fn default() -> Self {
873 Self::new()
874 }
875}
876
877impl RequestPacer {
878 pub fn new() -> Self {
887 let seed = SystemTime::now()
888 .duration_since(UNIX_EPOCH)
889 .map(|d| d.as_secs() ^ u64::from(d.subsec_nanos()))
890 .unwrap_or(0xdead_beef_cafe_1337);
891 Self {
892 rng: seed,
893 mean_ms: 1_200,
894 std_ms: 400,
895 min_ms: 400,
896 max_ms: 4_000,
897 last_request: None,
898 }
899 }
900
901 pub fn with_timing(mean_ms: u64, std_ms: u64, min_ms: u64, max_ms: u64) -> Self {
913 let (min_ms, max_ms) = if min_ms <= max_ms {
914 (min_ms, max_ms)
915 } else {
916 (max_ms, min_ms)
917 };
918 let seed = SystemTime::now()
919 .duration_since(UNIX_EPOCH)
920 .map(|d| d.as_secs() ^ u64::from(d.subsec_nanos()))
921 .unwrap_or(0xdead_beef_cafe_1337);
922 Self {
923 rng: seed,
924 mean_ms,
925 std_ms,
926 min_ms,
927 max_ms,
928 last_request: None,
929 }
930 }
931
932 pub fn with_rate(requests_per_second: f64) -> Self {
946 let mean_ms = (1_000.0 / requests_per_second.max(0.01)).max(1.0) as u64;
947 let std_ms = mean_ms / 4;
948 let min_ms = mean_ms / 2;
949 let max_ms = mean_ms.saturating_mul(2);
950 let seed = SystemTime::now()
951 .duration_since(UNIX_EPOCH)
952 .map(|d| d.as_secs() ^ u64::from(d.subsec_nanos()))
953 .unwrap_or(0xdead_beef_cafe_1337);
954 Self {
955 rng: seed,
956 mean_ms,
957 std_ms,
958 min_ms,
959 max_ms,
960 last_request: None,
961 }
962 }
963
964 pub async fn throttle(&mut self) {
980 let target_ms = rand_normal(&mut self.rng, self.mean_ms as f64, self.std_ms as f64)
981 .max(self.min_ms as f64)
982 .min(self.max_ms as f64) as u64;
983
984 if let Some(last) = self.last_request {
985 let elapsed_ms = last.elapsed().as_millis() as u64;
986 if elapsed_ms < target_ms {
987 sleep(Duration::from_millis(target_ms - elapsed_ms)).await;
988 }
989 }
990 self.last_request = Some(Instant::now());
991 }
992}
993
994#[cfg(test)]
997mod tests {
998 use super::*;
999
1000 #[test]
1001 fn mouse_simulator_starts_at_origin() {
1002 let mouse = MouseSimulator::new();
1003 assert_eq!(mouse.position(), (0.0, 0.0));
1004 }
1005
1006 #[test]
1007 fn mouse_simulator_with_seed_and_position() {
1008 let mouse = MouseSimulator::with_seed_and_position(42, 150.0, 300.0);
1009 assert_eq!(mouse.position(), (150.0, 300.0));
1010 }
1011
1012 #[test]
1013 fn compute_path_minimum_steps_for_zero_distance() {
1014 let mut mouse = MouseSimulator::with_seed_and_position(1, 100.0, 100.0);
1015 let path = mouse.compute_path(100.0, 100.0, 100.0, 100.0);
1016 assert!(path.len() >= 13);
1018 }
1019
1020 #[test]
1021 fn compute_path_scales_with_distance() {
1022 let mut mouse_near = MouseSimulator::with_seed_and_position(1, 0.0, 0.0);
1023 let mut mouse_far = MouseSimulator::with_seed_and_position(1, 0.0, 0.0);
1024
1025 let short_path = mouse_near.compute_path(0.0, 0.0, 30.0, 0.0);
1026 let long_path = mouse_far.compute_path(0.0, 0.0, 800.0, 0.0);
1027
1028 assert!(long_path.len() > short_path.len());
1030 }
1031
1032 #[test]
1033 fn compute_path_step_cap_at_120() {
1034 let mut mouse = MouseSimulator::with_seed_and_position(99, 0.0, 0.0);
1035 let path = mouse.compute_path(0.0, 0.0, 10_000.0, 0.0);
1037 assert!(path.len() <= 121);
1039 }
1040
1041 #[test]
1042 fn compute_path_endpoint_near_target() {
1043 let mut mouse = MouseSimulator::with_seed_and_position(7, 0.0, 0.0);
1044 let target_x = 500.0_f64;
1045 let target_y = 300.0_f64;
1046 let path = mouse.compute_path(0.0, 0.0, target_x, target_y);
1047 let last = path.last().copied().unwrap_or_default();
1048 assert!(
1050 (last.0 - target_x).abs() < 5.0,
1051 "x off by {}",
1052 (last.0 - target_x).abs()
1053 );
1054 assert!(
1055 (last.1 - target_y).abs() < 5.0,
1056 "y off by {}",
1057 (last.1 - target_y).abs()
1058 );
1059 }
1060
1061 #[test]
1062 fn compute_path_startpoint_near_origin() {
1063 let mut mouse = MouseSimulator::with_seed_and_position(3, 50.0, 80.0);
1064 let path = mouse.compute_path(50.0, 80.0, 400.0, 200.0);
1065 if let Some(first) = path.first() {
1067 assert!((first.0 - 50.0).abs() < 5.0);
1068 assert!((first.1 - 80.0).abs() < 5.0);
1069 }
1070 }
1071
1072 #[test]
1073 fn compute_path_diagonal_movement() {
1074 let mut mouse = MouseSimulator::with_seed_and_position(17, 0.0, 0.0);
1075 let path = mouse.compute_path(0.0, 0.0, 300.0, 400.0);
1076 assert!(path.len() >= 13);
1078 let last = path.last().copied().unwrap_or_default();
1079 assert!((last.0 - 300.0).abs() < 5.0);
1080 assert!((last.1 - 400.0).abs() < 5.0);
1081 }
1082
1083 #[test]
1084 fn compute_path_deterministic_with_same_seed() {
1085 let mut m1 = MouseSimulator::with_seed_and_position(42, 0.0, 0.0);
1086 let mut m2 = MouseSimulator::with_seed_and_position(42, 0.0, 0.0);
1087 let path1 = m1.compute_path(0.0, 0.0, 200.0, 150.0);
1088 let path2 = m2.compute_path(0.0, 0.0, 200.0, 150.0);
1089 assert_eq!(path1.len(), path2.len());
1090 for (a, b) in path1.iter().zip(path2.iter()) {
1091 assert!((a.0 - b.0).abs() < 1e-9);
1092 assert!((a.1 - b.1).abs() < 1e-9);
1093 }
1094 }
1095
1096 #[test]
1097 fn cubic_bezier_at_t0_is_p0() {
1098 let p0 = (10.0, 20.0);
1099 let p1 = (50.0, 100.0);
1100 let p2 = (150.0, 80.0);
1101 let p3 = (200.0, 30.0);
1102 let result = cubic_bezier(p0, p1, p2, p3, 0.0);
1103 assert!((result.0 - p0.0).abs() < 1e-9);
1104 assert!((result.1 - p0.1).abs() < 1e-9);
1105 }
1106
1107 #[test]
1108 fn cubic_bezier_at_t1_is_p3() {
1109 let p0 = (10.0, 20.0);
1110 let p1 = (50.0, 100.0);
1111 let p2 = (150.0, 80.0);
1112 let p3 = (200.0, 30.0);
1113 let result = cubic_bezier(p0, p1, p2, p3, 1.0);
1114 assert!((result.0 - p3.0).abs() < 1e-9);
1115 assert!((result.1 - p3.1).abs() < 1e-9);
1116 }
1117
1118 #[test]
1119 fn rand_f64_is_in_unit_interval() {
1120 let mut state = 12345u64;
1121 for _ in 0..1000 {
1122 let v = rand_f64(&mut state);
1123 assert!((0.0..1.0).contains(&v), "out of range: {v}");
1124 }
1125 }
1126
1127 #[test]
1128 fn rand_range_stays_in_bounds() {
1129 let mut state = 99999u64;
1130 for _ in 0..1000 {
1131 let v = rand_range(&mut state, 10.0, 50.0);
1132 assert!((10.0..50.0).contains(&v), "out of range: {v}");
1133 }
1134 }
1135
1136 #[test]
1137 fn typing_simulator_keystroke_delay_is_positive() {
1138 let mut ts = TypingSimulator::new();
1139 assert!(ts.keystroke_delay().as_millis() > 0);
1140 }
1141
1142 #[test]
1143 fn typing_simulator_keystroke_delay_in_range() {
1144 let mut ts = TypingSimulator::with_seed(123);
1145 for _ in 0..50 {
1146 let d = ts.keystroke_delay();
1147 assert!(
1148 d.as_millis() >= 30 && d.as_millis() <= 200,
1149 "delay out of range: {}ms",
1150 d.as_millis()
1151 );
1152 }
1153 }
1154
1155 #[test]
1156 fn typing_simulator_error_rate_clamps_to_one() {
1157 let ts = TypingSimulator::new().with_error_rate(2.0);
1158 assert!(
1159 (ts.error_rate - 1.0).abs() < 1e-9,
1160 "rate should clamp to 1.0"
1161 );
1162 }
1163
1164 #[test]
1165 fn typing_simulator_error_rate_clamps_to_zero() {
1166 let ts = TypingSimulator::new().with_error_rate(-0.5);
1167 assert!(ts.error_rate.abs() < 1e-9, "rate should clamp to 0.0");
1168 }
1169
1170 #[test]
1171 fn typing_simulator_deterministic_with_same_seed() {
1172 let mut t1 = TypingSimulator::with_seed(999);
1173 let mut t2 = TypingSimulator::with_seed(999);
1174 assert_eq!(t1.keystroke_delay(), t2.keystroke_delay());
1175 }
1176
1177 #[test]
1178 fn adjacent_key_returns_different_char() {
1179 let mut rng = 42u64;
1180 for &ch in &['a', 'b', 's', 'k', 'z', 'm'] {
1181 let adj = adjacent_key(ch, &mut rng);
1182 assert_ne!(adj, ch, "adjacent_key({ch}) should not return itself");
1183 }
1184 }
1185
1186 #[test]
1187 fn adjacent_key_preserves_case() {
1188 let mut rng = 7u64;
1189 let adj = adjacent_key('A', &mut rng);
1190 assert!(
1191 adj.is_uppercase(),
1192 "adjacent_key('A') should return uppercase"
1193 );
1194 }
1195
1196 #[test]
1197 fn adjacent_key_non_alpha_returns_fallback() {
1198 let mut rng = 1u64;
1199 assert_eq!(adjacent_key('!', &mut rng), 'x');
1200 assert_eq!(adjacent_key('5', &mut rng), 'x');
1201 }
1202
1203 #[test]
1204 fn interaction_level_default_is_none() {
1205 assert_eq!(InteractionLevel::default(), InteractionLevel::None);
1206 }
1207
1208 #[test]
1209 fn interaction_simulator_with_seed_is_deterministic() {
1210 let s1 = InteractionSimulator::with_seed(77, InteractionLevel::Low);
1211 let s2 = InteractionSimulator::with_seed(77, InteractionLevel::Low);
1212 assert_eq!(s1.rng, s2.rng);
1213 }
1214
1215 #[test]
1216 fn interaction_simulator_default_is_none_level() {
1217 let sim = InteractionSimulator::default();
1218 assert_eq!(sim.level, InteractionLevel::None);
1219 }
1220
1221 #[test]
1222 fn request_pacer_new_has_expected_defaults() {
1223 let p = RequestPacer::new();
1224 assert_eq!(p.mean_ms, 1_200);
1225 assert_eq!(p.min_ms, 400);
1226 assert_eq!(p.max_ms, 4_000);
1227 assert!(p.last_request.is_none());
1228 }
1229
1230 #[test]
1231 fn request_pacer_with_timing_stores_params() {
1232 let p = RequestPacer::with_timing(500, 100, 200, 2_000);
1233 assert_eq!(p.mean_ms, 500);
1234 assert_eq!(p.std_ms, 100);
1235 assert_eq!(p.min_ms, 200);
1236 assert_eq!(p.max_ms, 2_000);
1237 }
1238
1239 #[test]
1240 fn request_pacer_with_rate_computes_mean() {
1241 let p = RequestPacer::with_rate(0.5);
1243 assert_eq!(p.mean_ms, 2_000);
1244 assert_eq!(p.min_ms, 1_000);
1245 assert_eq!(p.max_ms, 4_000);
1246 }
1247
1248 #[test]
1249 fn request_pacer_with_rate_clamps_extreme() {
1250 let p = RequestPacer::with_rate(10_000.0);
1252 assert!(p.mean_ms >= 1);
1253 }
1254
1255 #[test]
1256 fn request_pacer_with_timing_swaps_inverted_bounds() {
1257 let p = RequestPacer::with_timing(500, 100, 2_000, 200);
1258 assert_eq!(p.min_ms, 200);
1259 assert_eq!(p.max_ms, 2_000);
1260 }
1261
1262 #[tokio::test]
1263 async fn request_pacer_throttle_first_immediate_then_waits() {
1264 let mut p = RequestPacer::with_timing(25, 0, 25, 25);
1265
1266 p.throttle().await;
1268
1269 let started = Instant::now();
1271 p.throttle().await;
1272 assert!(
1273 started.elapsed() >= Duration::from_millis(15),
1274 "second throttle should wait before returning"
1275 );
1276 }
1277}