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, SystemTime, UNIX_EPOCH};
24use tokio::time::sleep;
25
26use crate::error::{BrowserError, Result};
27
28const fn splitmix64(state: &mut u64) -> u64 {
32 *state = state.wrapping_add(0x9e37_79b9_7f4a_7c15);
33 let mut z = *state;
34 z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
35 z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
36 z ^ (z >> 31)
37}
38
39fn rand_f64(state: &mut u64) -> f64 {
41 (splitmix64(state) >> 11) as f64 / (1u64 << 53) as f64
42}
43
44fn rand_range(state: &mut u64, min: f64, max: f64) -> f64 {
46 rand_f64(state).mul_add(max - min, min)
47}
48
49fn rand_normal(state: &mut u64, mean: f64, std_dev: f64) -> f64 {
51 let u1 = rand_f64(state).max(1e-10);
52 let u2 = rand_f64(state);
53 let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
54 std_dev.mul_add(z, mean)
55}
56
57fn lerp(p0: (f64, f64), p1: (f64, f64), t: f64) -> (f64, f64) {
60 (t.mul_add(p1.0 - p0.0, p0.0), t.mul_add(p1.1 - p0.1, p0.1))
61}
62
63fn cubic_bezier(
65 p0: (f64, f64),
66 p1: (f64, f64),
67 p2: (f64, f64),
68 p3: (f64, f64),
69 t: f64,
70) -> (f64, f64) {
71 let a = lerp(p0, p1, t);
72 let b = lerp(p1, p2, t);
73 let c = lerp(p2, p3, t);
74 lerp(lerp(a, b, t), lerp(b, c, t), t)
75}
76
77pub struct MouseSimulator {
100 current_x: f64,
102 current_y: f64,
104 rng: u64,
106}
107
108impl Default for MouseSimulator {
109 fn default() -> Self {
110 Self::new()
111 }
112}
113
114impl MouseSimulator {
115 pub fn new() -> Self {
125 let seed = SystemTime::now()
126 .duration_since(UNIX_EPOCH)
127 .map(|d| d.as_secs() ^ u64::from(d.subsec_nanos()))
128 .unwrap_or(0x1234_5678_9abc_def0);
129 Self {
130 current_x: 0.0,
131 current_y: 0.0,
132 rng: seed,
133 }
134 }
135
136 pub const fn with_seed_and_position(seed: u64, x: f64, y: f64) -> Self {
148 Self {
149 current_x: x,
150 current_y: y,
151 rng: seed,
152 }
153 }
154
155 pub const fn position(&self) -> (f64, f64) {
166 (self.current_x, self.current_y)
167 }
168
169 pub fn compute_path(
195 &mut self,
196 from_x: f64,
197 from_y: f64,
198 to_x: f64,
199 to_y: f64,
200 ) -> Vec<(f64, f64)> {
201 let dx = to_x - from_x;
202 let dy = to_y - from_y;
203 let distance = dx.hypot(dy);
204
205 let steps = ((distance / 8.0).round() as usize).clamp(12, 120);
207
208 let (px, py) = if distance > 1.0 {
210 (-dy / distance, dx / distance)
211 } else {
212 (1.0, 0.0)
213 };
214
215 let offset_scale = (distance * 0.35).min(200.0);
217 let cp1_off = rand_normal(&mut self.rng, 0.0, offset_scale * 0.5);
218 let cp2_off = rand_normal(&mut self.rng, 0.0, offset_scale * 0.4);
219
220 let cp1 = (
222 px.mul_add(cp1_off, from_x + dx / 3.0),
223 py.mul_add(cp1_off, from_y + dy / 3.0),
224 );
225 let cp2 = (
226 px.mul_add(cp2_off, from_x + 2.0 * dx / 3.0),
227 py.mul_add(cp2_off, from_y + 2.0 * dy / 3.0),
228 );
229 let p0 = (from_x, from_y);
230 let p3 = (to_x, to_y);
231
232 (0..=steps)
233 .map(|i| {
234 let t = i as f64 / steps as f64;
235 let (bx, by) = cubic_bezier(p0, cp1, cp2, p3, t);
236 let jx = rand_normal(&mut self.rng, 0.0, 0.4);
238 let jy = rand_normal(&mut self.rng, 0.0, 0.4);
239 (bx + jx, by + jy)
240 })
241 .collect()
242 }
243
244 pub async fn move_to(&mut self, page: &Page, to_x: f64, to_y: f64) -> Result<()> {
254 use chromiumoxide::cdp::browser_protocol::input::{
255 DispatchMouseEventParams, DispatchMouseEventType,
256 };
257
258 let path = self.compute_path(self.current_x, self.current_y, to_x, to_y);
259
260 for &(x, y) in &path {
261 let params = DispatchMouseEventParams::builder()
262 .r#type(DispatchMouseEventType::MouseMoved)
263 .x(x)
264 .y(y)
265 .build()
266 .map_err(BrowserError::ConfigError)?;
267
268 page.execute(params)
269 .await
270 .map_err(|e| BrowserError::CdpError {
271 operation: "Input.dispatchMouseEvent(mouseMoved)".to_string(),
272 message: e.to_string(),
273 })?;
274
275 let delay_ms = rand_range(&mut self.rng, 10.0, 50.0) as u64;
276 sleep(Duration::from_millis(delay_ms)).await;
277 }
278
279 self.current_x = to_x;
280 self.current_y = to_y;
281 Ok(())
282 }
283
284 pub async fn click(&mut self, page: &Page, x: f64, y: f64) -> Result<()> {
293 use chromiumoxide::cdp::browser_protocol::input::{
294 DispatchMouseEventParams, DispatchMouseEventType, MouseButton,
295 };
296
297 self.move_to(page, x, y).await?;
298
299 let pre_ms = rand_range(&mut self.rng, 20.0, 80.0) as u64;
301 sleep(Duration::from_millis(pre_ms)).await;
302
303 let press = DispatchMouseEventParams::builder()
304 .r#type(DispatchMouseEventType::MousePressed)
305 .x(x)
306 .y(y)
307 .button(MouseButton::Left)
308 .click_count(1i64)
309 .build()
310 .map_err(BrowserError::ConfigError)?;
311
312 page.execute(press)
313 .await
314 .map_err(|e| BrowserError::CdpError {
315 operation: "Input.dispatchMouseEvent(mousePressed)".to_string(),
316 message: e.to_string(),
317 })?;
318
319 let hold_ms = rand_range(&mut self.rng, 50.0, 150.0) as u64;
321 sleep(Duration::from_millis(hold_ms)).await;
322
323 let release = DispatchMouseEventParams::builder()
324 .r#type(DispatchMouseEventType::MouseReleased)
325 .x(x)
326 .y(y)
327 .button(MouseButton::Left)
328 .click_count(1i64)
329 .build()
330 .map_err(BrowserError::ConfigError)?;
331
332 page.execute(release)
333 .await
334 .map_err(|e| BrowserError::CdpError {
335 operation: "Input.dispatchMouseEvent(mouseReleased)".to_string(),
336 message: e.to_string(),
337 })?;
338
339 Ok(())
340 }
341}
342
343fn adjacent_key(ch: char, rng: &mut u64) -> char {
350 const ROWS: [&str; 3] = ["qwertyuiop", "asdfghjkl", "zxcvbnm"];
351 let lc = ch.to_lowercase().next().unwrap_or(ch);
352 for row in ROWS {
353 let chars: Vec<char> = row.chars().collect();
354 if let Some(idx) = chars.iter().position(|&c| c == lc) {
355 let adj = if idx == 0 {
356 chars.get(1).copied().unwrap_or(lc)
357 } else if idx == chars.len() - 1 || rand_f64(rng) < 0.5 {
358 chars.get(idx - 1).copied().unwrap_or(lc)
359 } else {
360 chars.get(idx + 1).copied().unwrap_or(lc)
361 };
362 return if ch.is_uppercase() {
363 adj.to_uppercase().next().unwrap_or(adj)
364 } else {
365 adj
366 };
367 }
368 }
369 'x'
370}
371
372pub struct TypingSimulator {
394 rng: u64,
396 error_rate: f64,
398}
399
400impl Default for TypingSimulator {
401 fn default() -> Self {
402 Self::new()
403 }
404}
405
406impl TypingSimulator {
407 pub fn new() -> Self {
416 let seed = SystemTime::now()
417 .duration_since(UNIX_EPOCH)
418 .map(|d| d.as_secs() ^ u64::from(d.subsec_nanos()))
419 .unwrap_or(0xdead_beef_cafe_babe);
420 Self {
421 rng: seed,
422 error_rate: 0.015,
423 }
424 }
425
426 pub const fn with_seed(seed: u64) -> Self {
435 Self {
436 rng: seed,
437 error_rate: 0.015,
438 }
439 }
440
441 #[must_use]
452 pub const fn with_error_rate(mut self, rate: f64) -> Self {
453 self.error_rate = rate.clamp(0.0, 1.0);
454 self
455 }
456
457 pub fn keystroke_delay(&mut self) -> Duration {
470 let ms = rand_normal(&mut self.rng, 80.0, 25.0).clamp(30.0, 200.0) as u64;
471 Duration::from_millis(ms)
472 }
473
474 async fn dispatch_key(
476 page: &Page,
477 kind: DispatchKeyEventType,
478 key: &str,
479 text: Option<&str>,
480 modifiers: i64,
481 ) -> Result<()> {
482 let mut b = DispatchKeyEventParams::builder().r#type(kind).key(key);
483 if let Some(t) = text {
484 b = b.text(t);
485 }
486 if modifiers != 0 {
487 b = b.modifiers(modifiers);
488 }
489 let params = b.build().map_err(BrowserError::ConfigError)?;
490 page.execute(params)
491 .await
492 .map_err(|e| BrowserError::CdpError {
493 operation: "Input.dispatchKeyEvent".to_string(),
494 message: e.to_string(),
495 })?;
496 Ok(())
497 }
498
499 async fn type_backspace(page: &Page) -> Result<()> {
501 Self::dispatch_key(page, DispatchKeyEventType::RawKeyDown, "Backspace", None, 0).await?;
502 Self::dispatch_key(page, DispatchKeyEventType::KeyUp, "Backspace", None, 0).await?;
503 Ok(())
504 }
505
506 async fn type_char(page: &Page, ch: char) -> Result<()> {
511 let text = ch.to_string();
512 let modifiers: i64 = if ch.is_uppercase() && ch.is_alphabetic() {
513 8
514 } else {
515 0
516 };
517 let key = text.as_str();
518 Self::dispatch_key(
519 page,
520 DispatchKeyEventType::KeyDown,
521 key,
522 Some(&text),
523 modifiers,
524 )
525 .await?;
526 Self::dispatch_key(
527 page,
528 DispatchKeyEventType::Char,
529 key,
530 Some(&text),
531 modifiers,
532 )
533 .await?;
534 Self::dispatch_key(page, DispatchKeyEventType::KeyUp, key, None, modifiers).await?;
535 Ok(())
536 }
537
538 pub async fn type_text(&mut self, page: &Page, text: &str) -> Result<()> {
549 for ch in text.chars() {
550 if rand_f64(&mut self.rng) < self.error_rate {
552 let wrong = adjacent_key(ch, &mut self.rng);
553 Self::type_char(page, wrong).await?;
554 let typo_delay = rand_normal(&mut self.rng, 120.0, 30.0).clamp(60.0, 250.0) as u64;
555 sleep(Duration::from_millis(typo_delay)).await;
556 Self::type_backspace(page).await?;
557 let fix_delay = rand_range(&mut self.rng, 40.0, 120.0) as u64;
558 sleep(Duration::from_millis(fix_delay)).await;
559 }
560
561 Self::type_char(page, ch).await?;
562 sleep(self.keystroke_delay()).await;
563
564 if ch == ' ' || ch == '\n' {
566 let word_pause = rand_range(&mut self.rng, 100.0, 400.0) as u64;
567 sleep(Duration::from_millis(word_pause)).await;
568 }
569 }
570 Ok(())
571 }
572}
573
574#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
585pub enum InteractionLevel {
586 #[default]
588 None,
589 Low,
591 Medium,
593 High,
595}
596
597pub struct InteractionSimulator {
616 rng: u64,
617 mouse: MouseSimulator,
618 level: InteractionLevel,
619}
620
621impl Default for InteractionSimulator {
622 fn default() -> Self {
623 Self::new(InteractionLevel::None)
624 }
625}
626
627impl InteractionSimulator {
628 pub fn new(level: InteractionLevel) -> Self {
637 let seed = SystemTime::now()
638 .duration_since(UNIX_EPOCH)
639 .map(|d| d.as_secs() ^ u64::from(d.subsec_nanos()))
640 .unwrap_or(0x0123_4567_89ab_cdef);
641 Self {
642 rng: seed,
643 mouse: MouseSimulator::with_seed_and_position(seed ^ 0xca11_ab1e, 400.0, 300.0),
644 level,
645 }
646 }
647
648 pub const fn with_seed(seed: u64, level: InteractionLevel) -> Self {
657 Self {
658 rng: seed,
659 mouse: MouseSimulator::with_seed_and_position(seed ^ 0xca11_ab1e, 400.0, 300.0),
660 level,
661 }
662 }
663
664 async fn js(page: &Page, expr: String) -> Result<()> {
666 page.evaluate(expr)
667 .await
668 .map_err(|e| BrowserError::CdpError {
669 operation: "Runtime.evaluate".to_string(),
670 message: e.to_string(),
671 })?;
672 Ok(())
673 }
674
675 async fn scroll(page: &Page, delta_y: i64) -> Result<()> {
677 Self::js(
678 page,
679 format!("window.scrollBy({{top:{delta_y},behavior:'smooth'}})"),
680 )
681 .await
682 }
683
684 async fn do_scroll(&mut self, page: &Page) -> Result<()> {
686 let down = rand_range(&mut self.rng, 200.0, 600.0) as i64;
687 Self::scroll(page, down).await?;
688 let pause = rand_range(&mut self.rng, 300.0, 1_000.0) as u64;
689 sleep(Duration::from_millis(pause)).await;
690 let up = -(rand_range(&mut self.rng, 50.0, (down as f64) * 0.4) as i64);
691 Self::scroll(page, up).await?;
692 Ok(())
693 }
694
695 async fn do_mouse_wiggle(&mut self, page: &Page, vw: f64, vh: f64) -> Result<()> {
697 let tx = rand_range(&mut self.rng, vw * 0.1, vw * 0.9);
698 let ty = rand_range(&mut self.rng, vh * 0.1, vh * 0.9);
699 self.mouse.move_to(page, tx, ty).await
700 }
701
702 pub async fn random_interaction(
720 &mut self,
721 page: &Page,
722 viewport_w: f64,
723 viewport_h: f64,
724 ) -> Result<()> {
725 match self.level {
726 InteractionLevel::None => {}
727 InteractionLevel::Low => {
728 self.do_scroll(page).await?;
729 let pause = rand_range(&mut self.rng, 500.0, 1_500.0) as u64;
730 sleep(Duration::from_millis(pause)).await;
731 }
732 InteractionLevel::Medium => {
733 self.do_scroll(page).await?;
734 let p1 = rand_range(&mut self.rng, 1_000.0, 3_000.0) as u64;
735 sleep(Duration::from_millis(p1)).await;
736 self.do_mouse_wiggle(page, viewport_w, viewport_h).await?;
737 let p2 = rand_range(&mut self.rng, 500.0, 2_000.0) as u64;
738 sleep(Duration::from_millis(p2)).await;
739 }
740 InteractionLevel::High => {
741 self.do_scroll(page).await?;
742 let p1 = rand_range(&mut self.rng, 1_000.0, 5_000.0) as u64;
743 sleep(Duration::from_millis(p1)).await;
744 self.do_mouse_wiggle(page, viewport_w, viewport_h).await?;
745 let p2 = rand_range(&mut self.rng, 800.0, 3_000.0) as u64;
746 sleep(Duration::from_millis(p2)).await;
747 self.do_mouse_wiggle(page, viewport_w, viewport_h).await?;
748 let p3 = rand_range(&mut self.rng, 500.0, 2_000.0) as u64;
749 sleep(Duration::from_millis(p3)).await;
750 if rand_f64(&mut self.rng) < 0.4 {
752 let up = -(rand_range(&mut self.rng, 50.0, 200.0) as i64);
753 Self::scroll(page, up).await?;
754 sleep(Duration::from_millis(500)).await;
755 }
756 }
757 }
758 Ok(())
759 }
760}
761
762#[cfg(test)]
765mod tests {
766 use super::*;
767
768 #[test]
769 fn mouse_simulator_starts_at_origin() {
770 let mouse = MouseSimulator::new();
771 assert_eq!(mouse.position(), (0.0, 0.0));
772 }
773
774 #[test]
775 fn mouse_simulator_with_seed_and_position() {
776 let mouse = MouseSimulator::with_seed_and_position(42, 150.0, 300.0);
777 assert_eq!(mouse.position(), (150.0, 300.0));
778 }
779
780 #[test]
781 fn compute_path_minimum_steps_for_zero_distance() {
782 let mut mouse = MouseSimulator::with_seed_and_position(1, 100.0, 100.0);
783 let path = mouse.compute_path(100.0, 100.0, 100.0, 100.0);
784 assert!(path.len() >= 13);
786 }
787
788 #[test]
789 fn compute_path_scales_with_distance() {
790 let mut mouse_near = MouseSimulator::with_seed_and_position(1, 0.0, 0.0);
791 let mut mouse_far = MouseSimulator::with_seed_and_position(1, 0.0, 0.0);
792
793 let short_path = mouse_near.compute_path(0.0, 0.0, 30.0, 0.0);
794 let long_path = mouse_far.compute_path(0.0, 0.0, 800.0, 0.0);
795
796 assert!(long_path.len() > short_path.len());
798 }
799
800 #[test]
801 fn compute_path_step_cap_at_120() {
802 let mut mouse = MouseSimulator::with_seed_and_position(99, 0.0, 0.0);
803 let path = mouse.compute_path(0.0, 0.0, 10_000.0, 0.0);
805 assert!(path.len() <= 121);
807 }
808
809 #[test]
810 fn compute_path_endpoint_near_target() {
811 let mut mouse = MouseSimulator::with_seed_and_position(7, 0.0, 0.0);
812 let target_x = 500.0_f64;
813 let target_y = 300.0_f64;
814 let path = mouse.compute_path(0.0, 0.0, target_x, target_y);
815 let last = path.last().copied().unwrap_or_default();
816 assert!(
818 (last.0 - target_x).abs() < 5.0,
819 "x off by {}",
820 (last.0 - target_x).abs()
821 );
822 assert!(
823 (last.1 - target_y).abs() < 5.0,
824 "y off by {}",
825 (last.1 - target_y).abs()
826 );
827 }
828
829 #[test]
830 fn compute_path_startpoint_near_origin() {
831 let mut mouse = MouseSimulator::with_seed_and_position(3, 50.0, 80.0);
832 let path = mouse.compute_path(50.0, 80.0, 400.0, 200.0);
833 if let Some(first) = path.first() {
835 assert!((first.0 - 50.0).abs() < 5.0);
836 assert!((first.1 - 80.0).abs() < 5.0);
837 }
838 }
839
840 #[test]
841 fn compute_path_diagonal_movement() {
842 let mut mouse = MouseSimulator::with_seed_and_position(17, 0.0, 0.0);
843 let path = mouse.compute_path(0.0, 0.0, 300.0, 400.0);
844 assert!(path.len() >= 13);
846 let last = path.last().copied().unwrap_or_default();
847 assert!((last.0 - 300.0).abs() < 5.0);
848 assert!((last.1 - 400.0).abs() < 5.0);
849 }
850
851 #[test]
852 fn compute_path_deterministic_with_same_seed() {
853 let mut m1 = MouseSimulator::with_seed_and_position(42, 0.0, 0.0);
854 let mut m2 = MouseSimulator::with_seed_and_position(42, 0.0, 0.0);
855 let path1 = m1.compute_path(0.0, 0.0, 200.0, 150.0);
856 let path2 = m2.compute_path(0.0, 0.0, 200.0, 150.0);
857 assert_eq!(path1.len(), path2.len());
858 for (a, b) in path1.iter().zip(path2.iter()) {
859 assert!((a.0 - b.0).abs() < 1e-9);
860 assert!((a.1 - b.1).abs() < 1e-9);
861 }
862 }
863
864 #[test]
865 fn cubic_bezier_at_t0_is_p0() {
866 let p0 = (10.0, 20.0);
867 let p1 = (50.0, 100.0);
868 let p2 = (150.0, 80.0);
869 let p3 = (200.0, 30.0);
870 let result = cubic_bezier(p0, p1, p2, p3, 0.0);
871 assert!((result.0 - p0.0).abs() < 1e-9);
872 assert!((result.1 - p0.1).abs() < 1e-9);
873 }
874
875 #[test]
876 fn cubic_bezier_at_t1_is_p3() {
877 let p0 = (10.0, 20.0);
878 let p1 = (50.0, 100.0);
879 let p2 = (150.0, 80.0);
880 let p3 = (200.0, 30.0);
881 let result = cubic_bezier(p0, p1, p2, p3, 1.0);
882 assert!((result.0 - p3.0).abs() < 1e-9);
883 assert!((result.1 - p3.1).abs() < 1e-9);
884 }
885
886 #[test]
887 fn rand_f64_is_in_unit_interval() {
888 let mut state = 12345u64;
889 for _ in 0..1000 {
890 let v = rand_f64(&mut state);
891 assert!((0.0..1.0).contains(&v), "out of range: {v}");
892 }
893 }
894
895 #[test]
896 fn rand_range_stays_in_bounds() {
897 let mut state = 99999u64;
898 for _ in 0..1000 {
899 let v = rand_range(&mut state, 10.0, 50.0);
900 assert!((10.0..50.0).contains(&v), "out of range: {v}");
901 }
902 }
903
904 #[test]
905 fn typing_simulator_keystroke_delay_is_positive() {
906 let mut ts = TypingSimulator::new();
907 assert!(ts.keystroke_delay().as_millis() > 0);
908 }
909
910 #[test]
911 fn typing_simulator_keystroke_delay_in_range() {
912 let mut ts = TypingSimulator::with_seed(123);
913 for _ in 0..50 {
914 let d = ts.keystroke_delay();
915 assert!(
916 d.as_millis() >= 30 && d.as_millis() <= 200,
917 "delay out of range: {}ms",
918 d.as_millis()
919 );
920 }
921 }
922
923 #[test]
924 fn typing_simulator_error_rate_clamps_to_one() {
925 let ts = TypingSimulator::new().with_error_rate(2.0);
926 assert!(
927 (ts.error_rate - 1.0).abs() < 1e-9,
928 "rate should clamp to 1.0"
929 );
930 }
931
932 #[test]
933 fn typing_simulator_error_rate_clamps_to_zero() {
934 let ts = TypingSimulator::new().with_error_rate(-0.5);
935 assert!(ts.error_rate.abs() < 1e-9, "rate should clamp to 0.0");
936 }
937
938 #[test]
939 fn typing_simulator_deterministic_with_same_seed() {
940 let mut t1 = TypingSimulator::with_seed(999);
941 let mut t2 = TypingSimulator::with_seed(999);
942 assert_eq!(t1.keystroke_delay(), t2.keystroke_delay());
943 }
944
945 #[test]
946 fn adjacent_key_returns_different_char() {
947 let mut rng = 42u64;
948 for &ch in &['a', 'b', 's', 'k', 'z', 'm'] {
949 let adj = adjacent_key(ch, &mut rng);
950 assert_ne!(adj, ch, "adjacent_key({ch}) should not return itself");
951 }
952 }
953
954 #[test]
955 fn adjacent_key_preserves_case() {
956 let mut rng = 7u64;
957 let adj = adjacent_key('A', &mut rng);
958 assert!(
959 adj.is_uppercase(),
960 "adjacent_key('A') should return uppercase"
961 );
962 }
963
964 #[test]
965 fn adjacent_key_non_alpha_returns_fallback() {
966 let mut rng = 1u64;
967 assert_eq!(adjacent_key('!', &mut rng), 'x');
968 assert_eq!(adjacent_key('5', &mut rng), 'x');
969 }
970
971 #[test]
972 fn interaction_level_default_is_none() {
973 assert_eq!(InteractionLevel::default(), InteractionLevel::None);
974 }
975
976 #[test]
977 fn interaction_simulator_with_seed_is_deterministic() {
978 let s1 = InteractionSimulator::with_seed(77, InteractionLevel::Low);
979 let s2 = InteractionSimulator::with_seed(77, InteractionLevel::Low);
980 assert_eq!(s1.rng, s2.rng);
981 }
982
983 #[test]
984 fn interaction_simulator_default_is_none_level() {
985 let sim = InteractionSimulator::default();
986 assert_eq!(sim.level, InteractionLevel::None);
987 }
988}