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