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 #[must_use]
126 pub fn new() -> Self {
127 let seed = SystemTime::now()
128 .duration_since(UNIX_EPOCH)
129 .map_or(0x1234_5678_9abc_def0, |d| {
130 d.as_secs() ^ u64::from(d.subsec_nanos())
131 });
132 Self {
133 current_x: 0.0,
134 current_y: 0.0,
135 rng: seed,
136 }
137 }
138
139 #[must_use]
151 pub const fn with_seed_and_position(seed: u64, x: f64, y: f64) -> Self {
152 Self {
153 current_x: x,
154 current_y: y,
155 rng: seed,
156 }
157 }
158
159 #[must_use]
170 pub const fn position(&self) -> (f64, f64) {
171 (self.current_x, self.current_y)
172 }
173
174 pub fn compute_path(
200 &mut self,
201 from_x: f64,
202 from_y: f64,
203 to_x: f64,
204 to_y: f64,
205 ) -> Vec<(f64, f64)> {
206 let dx = to_x - from_x;
207 let dy = to_y - from_y;
208 let distance = dx.hypot(dy);
209
210 let steps = ((distance / 8.0).round() as usize).clamp(12, 120);
212
213 let (px, py) = if distance > 1.0 {
215 (-dy / distance, dx / distance)
216 } else {
217 (1.0, 0.0)
218 };
219
220 let offset_scale = (distance * 0.35).min(200.0);
222 let cp1_off = rand_normal(&mut self.rng, 0.0, offset_scale * 0.5);
223 let cp2_off = rand_normal(&mut self.rng, 0.0, offset_scale * 0.4);
224
225 let cp1 = (
227 px.mul_add(cp1_off, from_x + dx / 3.0),
228 py.mul_add(cp1_off, from_y + dy / 3.0),
229 );
230 let cp2 = (
231 px.mul_add(cp2_off, from_x + 2.0 * dx / 3.0),
232 py.mul_add(cp2_off, from_y + 2.0 * dy / 3.0),
233 );
234 let p0 = (from_x, from_y);
235 let p3 = (to_x, to_y);
236
237 (0..=steps)
238 .map(|i| {
239 let t = i as f64 / steps as f64;
240 let (bx, by) = cubic_bezier(p0, cp1, cp2, p3, t);
241 let jx = rand_normal(&mut self.rng, 0.0, 0.4);
243 let jy = rand_normal(&mut self.rng, 0.0, 0.4);
244 (bx + jx, by + jy)
245 })
246 .collect()
247 }
248
249 pub async fn move_to(&mut self, page: &Page, to_x: f64, to_y: f64) -> Result<()> {
259 use chromiumoxide::cdp::browser_protocol::input::{
260 DispatchMouseEventParams, DispatchMouseEventType,
261 };
262
263 let path = self.compute_path(self.current_x, self.current_y, to_x, to_y);
264
265 for &(x, y) in &path {
266 let params = DispatchMouseEventParams::builder()
267 .r#type(DispatchMouseEventType::MouseMoved)
268 .x(x)
269 .y(y)
270 .build()
271 .map_err(BrowserError::ConfigError)?;
272
273 page.execute(params)
274 .await
275 .map_err(|e| BrowserError::CdpError {
276 operation: "Input.dispatchMouseEvent(mouseMoved)".to_string(),
277 message: e.to_string(),
278 })?;
279
280 let delay_ms = rand_range(&mut self.rng, 10.0, 50.0) as u64;
281 sleep(Duration::from_millis(delay_ms)).await;
282 }
283
284 self.current_x = to_x;
285 self.current_y = to_y;
286 Ok(())
287 }
288
289 pub async fn click(&mut self, page: &Page, x: f64, y: f64) -> Result<()> {
298 use chromiumoxide::cdp::browser_protocol::input::{
299 DispatchMouseEventParams, DispatchMouseEventType, MouseButton,
300 };
301
302 self.move_to(page, x, y).await?;
303
304 let pre_ms = rand_range(&mut self.rng, 20.0, 80.0) as u64;
306 sleep(Duration::from_millis(pre_ms)).await;
307
308 let press = DispatchMouseEventParams::builder()
309 .r#type(DispatchMouseEventType::MousePressed)
310 .x(x)
311 .y(y)
312 .button(MouseButton::Left)
313 .click_count(1i64)
314 .build()
315 .map_err(BrowserError::ConfigError)?;
316
317 page.execute(press)
318 .await
319 .map_err(|e| BrowserError::CdpError {
320 operation: "Input.dispatchMouseEvent(mousePressed)".to_string(),
321 message: e.to_string(),
322 })?;
323
324 let hold_ms = rand_range(&mut self.rng, 50.0, 150.0) as u64;
326 sleep(Duration::from_millis(hold_ms)).await;
327
328 let release = DispatchMouseEventParams::builder()
329 .r#type(DispatchMouseEventType::MouseReleased)
330 .x(x)
331 .y(y)
332 .button(MouseButton::Left)
333 .click_count(1i64)
334 .build()
335 .map_err(BrowserError::ConfigError)?;
336
337 page.execute(release)
338 .await
339 .map_err(|e| BrowserError::CdpError {
340 operation: "Input.dispatchMouseEvent(mouseReleased)".to_string(),
341 message: e.to_string(),
342 })?;
343
344 Ok(())
345 }
346}
347
348fn adjacent_key(ch: char, rng: &mut u64) -> char {
355 const ROWS: [&str; 3] = ["qwertyuiop", "asdfghjkl", "zxcvbnm"];
356 let lc = ch.to_lowercase().next().unwrap_or(ch);
357 for row in ROWS {
358 let chars: Vec<char> = row.chars().collect();
359 if let Some(idx) = chars.iter().position(|&c| c == lc) {
360 let adj = if idx == 0 {
361 chars.get(1).copied().unwrap_or(lc)
362 } else if idx == chars.len() - 1 || rand_f64(rng) < 0.5 {
363 chars.get(idx - 1).copied().unwrap_or(lc)
364 } else {
365 chars.get(idx + 1).copied().unwrap_or(lc)
366 };
367 return if ch.is_uppercase() {
368 adj.to_uppercase().next().unwrap_or(adj)
369 } else {
370 adj
371 };
372 }
373 }
374 'x'
375}
376
377pub struct TypingSimulator {
399 rng: u64,
401 error_rate: f64,
403}
404
405impl Default for TypingSimulator {
406 fn default() -> Self {
407 Self::new()
408 }
409}
410
411impl TypingSimulator {
412 #[must_use]
421 pub fn new() -> Self {
422 let seed = SystemTime::now()
423 .duration_since(UNIX_EPOCH)
424 .map_or(0xdead_beef_cafe_babe, |d| {
425 d.as_secs() ^ u64::from(d.subsec_nanos())
426 });
427 Self {
428 rng: seed,
429 error_rate: 0.015,
430 }
431 }
432
433 #[must_use]
442 pub const fn with_seed(seed: u64) -> Self {
443 Self {
444 rng: seed,
445 error_rate: 0.015,
446 }
447 }
448
449 #[must_use]
460 pub const fn with_error_rate(mut self, rate: f64) -> Self {
461 self.error_rate = rate.clamp(0.0, 1.0);
462 self
463 }
464
465 pub fn keystroke_delay(&mut self) -> Duration {
478 let ms = rand_normal(&mut self.rng, 80.0, 25.0).clamp(30.0, 200.0) as u64;
479 Duration::from_millis(ms)
480 }
481
482 async fn dispatch_key(
484 page: &Page,
485 kind: DispatchKeyEventType,
486 key: &str,
487 text: Option<&str>,
488 modifiers: i64,
489 ) -> Result<()> {
490 let mut b = DispatchKeyEventParams::builder().r#type(kind).key(key);
491 if let Some(t) = text {
492 b = b.text(t);
493 }
494 if modifiers != 0 {
495 b = b.modifiers(modifiers);
496 }
497 let params = b.build().map_err(BrowserError::ConfigError)?;
498 page.execute(params)
499 .await
500 .map_err(|e| BrowserError::CdpError {
501 operation: "Input.dispatchKeyEvent".to_string(),
502 message: e.to_string(),
503 })?;
504 Ok(())
505 }
506
507 async fn type_backspace(page: &Page) -> Result<()> {
509 Self::dispatch_key(page, DispatchKeyEventType::RawKeyDown, "Backspace", None, 0).await?;
510 Self::dispatch_key(page, DispatchKeyEventType::KeyUp, "Backspace", None, 0).await?;
511 Ok(())
512 }
513
514 async fn type_char(page: &Page, ch: char) -> Result<()> {
519 let text = ch.to_string();
520 let modifiers: i64 = if ch.is_uppercase() && ch.is_alphabetic() {
521 8
522 } else {
523 0
524 };
525 let key = text.as_str();
526 Self::dispatch_key(
527 page,
528 DispatchKeyEventType::KeyDown,
529 key,
530 Some(&text),
531 modifiers,
532 )
533 .await?;
534 Self::dispatch_key(
535 page,
536 DispatchKeyEventType::Char,
537 key,
538 Some(&text),
539 modifiers,
540 )
541 .await?;
542 Self::dispatch_key(page, DispatchKeyEventType::KeyUp, key, None, modifiers).await?;
543 Ok(())
544 }
545
546 pub async fn type_text(&mut self, page: &Page, text: &str) -> Result<()> {
557 for ch in text.chars() {
558 if rand_f64(&mut self.rng) < self.error_rate {
560 let wrong = adjacent_key(ch, &mut self.rng);
561 Self::type_char(page, wrong).await?;
562 let typo_delay = rand_normal(&mut self.rng, 120.0, 30.0).clamp(60.0, 250.0) as u64;
563 sleep(Duration::from_millis(typo_delay)).await;
564 Self::type_backspace(page).await?;
565 let fix_delay = rand_range(&mut self.rng, 40.0, 120.0) as u64;
566 sleep(Duration::from_millis(fix_delay)).await;
567 }
568
569 Self::type_char(page, ch).await?;
570 sleep(self.keystroke_delay()).await;
571
572 if ch == ' ' || ch == '\n' {
574 let word_pause = rand_range(&mut self.rng, 100.0, 400.0) as u64;
575 sleep(Duration::from_millis(word_pause)).await;
576 }
577 }
578 Ok(())
579 }
580}
581
582#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
593pub enum InteractionLevel {
594 #[default]
596 None,
597 Low,
599 Medium,
601 High,
603}
604
605pub struct InteractionSimulator {
624 rng: u64,
625 mouse: MouseSimulator,
626 level: InteractionLevel,
627}
628
629impl Default for InteractionSimulator {
630 fn default() -> Self {
631 Self::new(InteractionLevel::None)
632 }
633}
634
635impl InteractionSimulator {
636 #[must_use]
645 pub fn new(level: InteractionLevel) -> Self {
646 let seed = SystemTime::now()
647 .duration_since(UNIX_EPOCH)
648 .map_or(0x0123_4567_89ab_cdef, |d| {
649 d.as_secs() ^ u64::from(d.subsec_nanos())
650 });
651 Self {
652 rng: seed,
653 mouse: MouseSimulator::with_seed_and_position(seed ^ 0xca11_ab1e, 400.0, 300.0),
654 level,
655 }
656 }
657
658 #[must_use]
667 pub const fn with_seed(seed: u64, level: InteractionLevel) -> Self {
668 Self {
669 rng: seed,
670 mouse: MouseSimulator::with_seed_and_position(seed ^ 0xca11_ab1e, 400.0, 300.0),
671 level,
672 }
673 }
674
675 async fn js(page: &Page, expr: String) -> Result<()> {
677 page.evaluate(expr)
678 .await
679 .map_err(|e| BrowserError::CdpError {
680 operation: "Runtime.evaluate".to_string(),
681 message: e.to_string(),
682 })?;
683 Ok(())
684 }
685
686 async fn scroll(page: &Page, delta_y: i64) -> Result<()> {
688 Self::js(
689 page,
690 format!("window.scrollBy({{top:{delta_y},behavior:'smooth'}})"),
691 )
692 .await
693 }
694
695 async fn do_keyactivity(&mut self, page: &Page) -> Result<()> {
703 const KEYS: &[&str] = &["ArrowDown", "Tab", "ArrowRight", "ArrowUp"];
704 let count = 3 + rand_range(&mut self.rng, 0.0, 4.0) as u32;
705 let mut successful_pairs = 0u32;
706 for i in 0..count {
707 let key = KEYS
708 .get((i as usize) % KEYS.len())
709 .copied()
710 .unwrap_or("Tab");
711 let down_delay = rand_range(&mut self.rng, 50.0, 120.0) as u64;
712 sleep(Duration::from_millis(down_delay)).await;
713 let keydown_ok = if let Err(e) = Self::js(
714 page,
715 format!(
716 "window.dispatchEvent(new KeyboardEvent('keydown',\
717 {{bubbles:true,cancelable:true,key:{key:?},code:{key:?}}}));"
718 ),
719 )
720 .await
721 {
722 warn!(key, "Failed to dispatch keydown event: {e}");
723 false
724 } else {
725 true
726 };
727 let hold_ms = rand_range(&mut self.rng, 20.0, 60.0) as u64;
728 sleep(Duration::from_millis(hold_ms)).await;
729 let keyup_ok = if let Err(e) = Self::js(
730 page,
731 format!(
732 "window.dispatchEvent(new KeyboardEvent('keyup',\
733 {{bubbles:true,cancelable:true,key:{key:?},code:{key:?}}}));"
734 ),
735 )
736 .await
737 {
738 warn!(key, "Failed to dispatch keyup event: {e}");
739 false
740 } else {
741 true
742 };
743
744 if keydown_ok && keyup_ok {
745 successful_pairs += 1;
746 }
747 }
748
749 if successful_pairs == 0 {
750 return Err(BrowserError::CdpError {
751 operation: "InteractionSimulator::do_keyactivity".to_string(),
752 message: "all synthetic key event dispatches failed".to_string(),
753 });
754 }
755
756 Ok(())
757 }
758
759 async fn do_scroll(&mut self, page: &Page) -> Result<()> {
761 let down = rand_range(&mut self.rng, 200.0, 600.0) as i64;
762 Self::scroll(page, down).await?;
763 let pause = rand_range(&mut self.rng, 300.0, 1_000.0) as u64;
764 sleep(Duration::from_millis(pause)).await;
765 let up = -(rand_range(&mut self.rng, 50.0, (down as f64) * 0.4) as i64);
766 Self::scroll(page, up).await?;
767 Ok(())
768 }
769
770 async fn do_mouse_wiggle(&mut self, page: &Page, vw: f64, vh: f64) -> Result<()> {
772 let tx = rand_range(&mut self.rng, vw * 0.1, vw * 0.9);
773 let ty = rand_range(&mut self.rng, vh * 0.1, vh * 0.9);
774 self.mouse.move_to(page, tx, ty).await
775 }
776
777 pub async fn random_interaction(
795 &mut self,
796 page: &Page,
797 viewport_w: f64,
798 viewport_h: f64,
799 ) -> Result<()> {
800 match self.level {
801 InteractionLevel::None => {}
802 InteractionLevel::Low => {
803 self.do_scroll(page).await?;
804 let pause = rand_range(&mut self.rng, 500.0, 1_500.0) as u64;
805 sleep(Duration::from_millis(pause)).await;
806 }
807 InteractionLevel::Medium => {
808 self.do_scroll(page).await?;
809 let p1 = rand_range(&mut self.rng, 800.0, 2_000.0) as u64;
810 sleep(Duration::from_millis(p1)).await;
811 self.do_keyactivity(page).await?;
813 let p2 = rand_range(&mut self.rng, 500.0, 1_500.0) as u64;
814 sleep(Duration::from_millis(p2)).await;
815 self.do_mouse_wiggle(page, viewport_w, viewport_h).await?;
816 let p3 = rand_range(&mut self.rng, 400.0, 1_500.0) as u64;
817 sleep(Duration::from_millis(p3)).await;
818 }
819 InteractionLevel::High => {
820 self.do_scroll(page).await?;
821 let p1 = rand_range(&mut self.rng, 1_000.0, 5_000.0) as u64;
822 sleep(Duration::from_millis(p1)).await;
823 self.do_keyactivity(page).await?;
824 let p2 = rand_range(&mut self.rng, 400.0, 1_200.0) as u64;
825 sleep(Duration::from_millis(p2)).await;
826 self.do_mouse_wiggle(page, viewport_w, viewport_h).await?;
827 let p3 = rand_range(&mut self.rng, 800.0, 3_000.0) as u64;
828 sleep(Duration::from_millis(p3)).await;
829 self.do_keyactivity(page).await?;
830 let p4 = rand_range(&mut self.rng, 300.0, 800.0) as u64;
831 sleep(Duration::from_millis(p4)).await;
832 self.do_mouse_wiggle(page, viewport_w, viewport_h).await?;
833 let p5 = rand_range(&mut self.rng, 500.0, 2_000.0) as u64;
834 sleep(Duration::from_millis(p5)).await;
835 if rand_f64(&mut self.rng) < 0.4 {
837 let up = -(rand_range(&mut self.rng, 50.0, 200.0) as i64);
838 Self::scroll(page, up).await?;
839 sleep(Duration::from_millis(500)).await;
840 }
841 }
842 }
843 Ok(())
844 }
845}
846
847pub struct RequestPacer {
873 rng: u64,
874 mean_ms: u64,
875 std_ms: u64,
876 min_ms: u64,
877 max_ms: u64,
878 last_request: Option<Instant>,
879}
880
881impl Default for RequestPacer {
882 fn default() -> Self {
883 Self::new()
884 }
885}
886
887impl RequestPacer {
888 #[must_use]
897 pub fn new() -> Self {
898 let seed = SystemTime::now()
899 .duration_since(UNIX_EPOCH)
900 .map_or(0xdead_beef_cafe_1337, |d| {
901 d.as_secs() ^ u64::from(d.subsec_nanos())
902 });
903 Self {
904 rng: seed,
905 mean_ms: 1_200,
906 std_ms: 400,
907 min_ms: 400,
908 max_ms: 4_000,
909 last_request: None,
910 }
911 }
912
913 #[must_use]
925 pub fn with_timing(mean_ms: u64, std_ms: u64, min_ms: u64, max_ms: u64) -> Self {
926 let (min_ms, max_ms) = if min_ms <= max_ms {
927 (min_ms, max_ms)
928 } else {
929 (max_ms, min_ms)
930 };
931 let seed = SystemTime::now()
932 .duration_since(UNIX_EPOCH)
933 .map_or(0xdead_beef_cafe_1337, |d| {
934 d.as_secs() ^ u64::from(d.subsec_nanos())
935 });
936 Self {
937 rng: seed,
938 mean_ms,
939 std_ms,
940 min_ms,
941 max_ms,
942 last_request: None,
943 }
944 }
945
946 #[must_use]
960 pub fn with_rate(requests_per_second: f64) -> Self {
961 let mean_ms = (1_000.0 / requests_per_second.max(0.01)).max(1.0) as u64;
962 let std_ms = mean_ms / 4;
963 let min_ms = mean_ms / 2;
964 let max_ms = mean_ms.saturating_mul(2);
965 let seed = SystemTime::now()
966 .duration_since(UNIX_EPOCH)
967 .map_or(0xdead_beef_cafe_1337, |d| {
968 d.as_secs() ^ u64::from(d.subsec_nanos())
969 });
970 Self {
971 rng: seed,
972 mean_ms,
973 std_ms,
974 min_ms,
975 max_ms,
976 last_request: None,
977 }
978 }
979
980 pub async fn throttle(&mut self) {
996 let target_ms = rand_normal(&mut self.rng, self.mean_ms as f64, self.std_ms as f64)
997 .max(self.min_ms as f64)
998 .min(self.max_ms as f64) as u64;
999
1000 if let Some(last) = self.last_request {
1001 let elapsed_ms = last.elapsed().as_millis() as u64;
1002 if elapsed_ms < target_ms {
1003 sleep(Duration::from_millis(target_ms - elapsed_ms)).await;
1004 }
1005 }
1006 self.last_request = Some(Instant::now());
1007 }
1008}
1009
1010#[cfg(test)]
1013mod tests {
1014 use super::*;
1015
1016 #[test]
1017 fn mouse_simulator_starts_at_origin() {
1018 let mouse = MouseSimulator::new();
1019 assert_eq!(mouse.position(), (0.0, 0.0));
1020 }
1021
1022 #[test]
1023 fn mouse_simulator_with_seed_and_position() {
1024 let mouse = MouseSimulator::with_seed_and_position(42, 150.0, 300.0);
1025 assert_eq!(mouse.position(), (150.0, 300.0));
1026 }
1027
1028 #[test]
1029 fn compute_path_minimum_steps_for_zero_distance() {
1030 let mut mouse = MouseSimulator::with_seed_and_position(1, 100.0, 100.0);
1031 let path = mouse.compute_path(100.0, 100.0, 100.0, 100.0);
1032 assert!(path.len() >= 13);
1034 }
1035
1036 #[test]
1037 fn compute_path_scales_with_distance() {
1038 let mut mouse_near = MouseSimulator::with_seed_and_position(1, 0.0, 0.0);
1039 let mut mouse_far = MouseSimulator::with_seed_and_position(1, 0.0, 0.0);
1040
1041 let short_path = mouse_near.compute_path(0.0, 0.0, 30.0, 0.0);
1042 let long_path = mouse_far.compute_path(0.0, 0.0, 800.0, 0.0);
1043
1044 assert!(long_path.len() > short_path.len());
1046 }
1047
1048 #[test]
1049 fn compute_path_step_cap_at_120() {
1050 let mut mouse = MouseSimulator::with_seed_and_position(99, 0.0, 0.0);
1051 let path = mouse.compute_path(0.0, 0.0, 10_000.0, 0.0);
1053 assert!(path.len() <= 121);
1055 }
1056
1057 #[test]
1058 fn compute_path_endpoint_near_target() {
1059 let mut mouse = MouseSimulator::with_seed_and_position(7, 0.0, 0.0);
1060 let target_x = 500.0_f64;
1061 let target_y = 300.0_f64;
1062 let path = mouse.compute_path(0.0, 0.0, target_x, target_y);
1063 let last = path.last().copied().unwrap_or_default();
1064 assert!(
1066 (last.0 - target_x).abs() < 5.0,
1067 "x off by {}",
1068 (last.0 - target_x).abs()
1069 );
1070 assert!(
1071 (last.1 - target_y).abs() < 5.0,
1072 "y off by {}",
1073 (last.1 - target_y).abs()
1074 );
1075 }
1076
1077 #[test]
1078 fn compute_path_startpoint_near_origin() {
1079 let mut mouse = MouseSimulator::with_seed_and_position(3, 50.0, 80.0);
1080 let path = mouse.compute_path(50.0, 80.0, 400.0, 200.0);
1081 if let Some(first) = path.first() {
1083 assert!((first.0 - 50.0).abs() < 5.0);
1084 assert!((first.1 - 80.0).abs() < 5.0);
1085 }
1086 }
1087
1088 #[test]
1089 fn compute_path_diagonal_movement() {
1090 let mut mouse = MouseSimulator::with_seed_and_position(17, 0.0, 0.0);
1091 let path = mouse.compute_path(0.0, 0.0, 300.0, 400.0);
1092 assert!(path.len() >= 13);
1094 let last = path.last().copied().unwrap_or_default();
1095 assert!((last.0 - 300.0).abs() < 5.0);
1096 assert!((last.1 - 400.0).abs() < 5.0);
1097 }
1098
1099 #[test]
1100 fn compute_path_deterministic_with_same_seed() {
1101 let mut m1 = MouseSimulator::with_seed_and_position(42, 0.0, 0.0);
1102 let mut m2 = MouseSimulator::with_seed_and_position(42, 0.0, 0.0);
1103 let path1 = m1.compute_path(0.0, 0.0, 200.0, 150.0);
1104 let path2 = m2.compute_path(0.0, 0.0, 200.0, 150.0);
1105 assert_eq!(path1.len(), path2.len());
1106 for (a, b) in path1.iter().zip(path2.iter()) {
1107 assert!((a.0 - b.0).abs() < 1e-9);
1108 assert!((a.1 - b.1).abs() < 1e-9);
1109 }
1110 }
1111
1112 #[test]
1113 fn cubic_bezier_at_t0_is_p0() {
1114 let p0 = (10.0, 20.0);
1115 let p1 = (50.0, 100.0);
1116 let p2 = (150.0, 80.0);
1117 let p3 = (200.0, 30.0);
1118 let result = cubic_bezier(p0, p1, p2, p3, 0.0);
1119 assert!((result.0 - p0.0).abs() < 1e-9);
1120 assert!((result.1 - p0.1).abs() < 1e-9);
1121 }
1122
1123 #[test]
1124 fn cubic_bezier_at_t1_is_p3() {
1125 let p0 = (10.0, 20.0);
1126 let p1 = (50.0, 100.0);
1127 let p2 = (150.0, 80.0);
1128 let p3 = (200.0, 30.0);
1129 let result = cubic_bezier(p0, p1, p2, p3, 1.0);
1130 assert!((result.0 - p3.0).abs() < 1e-9);
1131 assert!((result.1 - p3.1).abs() < 1e-9);
1132 }
1133
1134 #[test]
1135 fn rand_f64_is_in_unit_interval() {
1136 let mut state = 12345u64;
1137 for _ in 0..1000 {
1138 let v = rand_f64(&mut state);
1139 assert!((0.0..1.0).contains(&v), "out of range: {v}");
1140 }
1141 }
1142
1143 #[test]
1144 fn rand_range_stays_in_bounds() {
1145 let mut state = 99999u64;
1146 for _ in 0..1000 {
1147 let v = rand_range(&mut state, 10.0, 50.0);
1148 assert!((10.0..50.0).contains(&v), "out of range: {v}");
1149 }
1150 }
1151
1152 #[test]
1153 fn typing_simulator_keystroke_delay_is_positive() {
1154 let mut ts = TypingSimulator::new();
1155 assert!(ts.keystroke_delay().as_millis() > 0);
1156 }
1157
1158 #[test]
1159 fn typing_simulator_keystroke_delay_in_range() {
1160 let mut ts = TypingSimulator::with_seed(123);
1161 for _ in 0..50 {
1162 let d = ts.keystroke_delay();
1163 assert!(
1164 d.as_millis() >= 30 && d.as_millis() <= 200,
1165 "delay out of range: {}ms",
1166 d.as_millis()
1167 );
1168 }
1169 }
1170
1171 #[test]
1172 fn typing_simulator_error_rate_clamps_to_one() {
1173 let ts = TypingSimulator::new().with_error_rate(2.0);
1174 assert!(
1175 (ts.error_rate - 1.0).abs() < 1e-9,
1176 "rate should clamp to 1.0"
1177 );
1178 }
1179
1180 #[test]
1181 fn typing_simulator_error_rate_clamps_to_zero() {
1182 let ts = TypingSimulator::new().with_error_rate(-0.5);
1183 assert!(ts.error_rate.abs() < 1e-9, "rate should clamp to 0.0");
1184 }
1185
1186 #[test]
1187 fn typing_simulator_deterministic_with_same_seed() {
1188 let mut t1 = TypingSimulator::with_seed(999);
1189 let mut t2 = TypingSimulator::with_seed(999);
1190 assert_eq!(t1.keystroke_delay(), t2.keystroke_delay());
1191 }
1192
1193 #[test]
1194 fn adjacent_key_returns_different_char() {
1195 let mut rng = 42u64;
1196 for &ch in &['a', 'b', 's', 'k', 'z', 'm'] {
1197 let adj = adjacent_key(ch, &mut rng);
1198 assert_ne!(adj, ch, "adjacent_key({ch}) should not return itself");
1199 }
1200 }
1201
1202 #[test]
1203 fn adjacent_key_preserves_case() {
1204 let mut rng = 7u64;
1205 let adj = adjacent_key('A', &mut rng);
1206 assert!(
1207 adj.is_uppercase(),
1208 "adjacent_key('A') should return uppercase"
1209 );
1210 }
1211
1212 #[test]
1213 fn adjacent_key_non_alpha_returns_fallback() {
1214 let mut rng = 1u64;
1215 assert_eq!(adjacent_key('!', &mut rng), 'x');
1216 assert_eq!(adjacent_key('5', &mut rng), 'x');
1217 }
1218
1219 #[test]
1220 fn interaction_level_default_is_none() {
1221 assert_eq!(InteractionLevel::default(), InteractionLevel::None);
1222 }
1223
1224 #[test]
1225 fn interaction_simulator_with_seed_is_deterministic() {
1226 let s1 = InteractionSimulator::with_seed(77, InteractionLevel::Low);
1227 let s2 = InteractionSimulator::with_seed(77, InteractionLevel::Low);
1228 assert_eq!(s1.rng, s2.rng);
1229 }
1230
1231 #[test]
1232 fn interaction_simulator_default_is_none_level() {
1233 let sim = InteractionSimulator::default();
1234 assert_eq!(sim.level, InteractionLevel::None);
1235 }
1236
1237 #[test]
1238 fn request_pacer_new_has_expected_defaults() {
1239 let p = RequestPacer::new();
1240 assert_eq!(p.mean_ms, 1_200);
1241 assert_eq!(p.min_ms, 400);
1242 assert_eq!(p.max_ms, 4_000);
1243 assert!(p.last_request.is_none());
1244 }
1245
1246 #[test]
1247 fn request_pacer_with_timing_stores_params() {
1248 let p = RequestPacer::with_timing(500, 100, 200, 2_000);
1249 assert_eq!(p.mean_ms, 500);
1250 assert_eq!(p.std_ms, 100);
1251 assert_eq!(p.min_ms, 200);
1252 assert_eq!(p.max_ms, 2_000);
1253 }
1254
1255 #[test]
1256 fn request_pacer_with_rate_computes_mean() {
1257 let p = RequestPacer::with_rate(0.5);
1259 assert_eq!(p.mean_ms, 2_000);
1260 assert_eq!(p.min_ms, 1_000);
1261 assert_eq!(p.max_ms, 4_000);
1262 }
1263
1264 #[test]
1265 fn request_pacer_with_rate_clamps_extreme() {
1266 let p = RequestPacer::with_rate(10_000.0);
1268 assert!(p.mean_ms >= 1);
1269 }
1270
1271 #[test]
1272 fn request_pacer_with_timing_swaps_inverted_bounds() {
1273 let p = RequestPacer::with_timing(500, 100, 2_000, 200);
1274 assert_eq!(p.min_ms, 200);
1275 assert_eq!(p.max_ms, 2_000);
1276 }
1277
1278 #[tokio::test]
1279 async fn request_pacer_throttle_first_immediate_then_waits() {
1280 let mut p = RequestPacer::with_timing(25, 0, 25, 25);
1281
1282 p.throttle().await;
1284
1285 let started = Instant::now();
1287 p.throttle().await;
1288 assert!(
1289 started.elapsed() >= Duration::from_millis(15),
1290 "second throttle should wait before returning"
1291 );
1292 }
1293}