use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum WaitStrategy {
Load,
DomContentLoaded,
NetworkIdle {
idle_ms: u64,
},
Selector {
css: String,
timeout_ms: u64,
},
Fixed {
ms: u64,
},
ReadingDwell {
wpm: u32,
jitter_ms: u64,
},
}
impl Default for WaitStrategy {
fn default() -> Self {
WaitStrategy::NetworkIdle { idle_ms: 500 }
}
}
pub fn compute_dwell_ms(
words: u64,
wpm: u32,
jitter_ms: u64,
min: u64,
max: u64,
rng: &mut rand::rngs::SmallRng,
) -> u64 {
let base_ms: f64 = if wpm == 0 {
0.0
} else {
(words as f64) / (wpm as f64) * 60_000.0
};
let jitter = gaussian(rng) * (jitter_ms as f64);
let total = (base_ms + jitter).max(0.0) as u64;
total.clamp(min, max)
}
fn gaussian(rng: &mut rand::rngs::SmallRng) -> f64 {
use rand::RngExt;
let u1 = ((rng.random::<u32>() as f64) / (u32::MAX as f64 + 1.0)).max(f64::MIN_POSITIVE);
let u2 = (rng.random::<u32>() as f64) / (u32::MAX as f64 + 1.0);
(-2.0_f64 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
}
#[cfg(test)]
mod tests {
use super::*;
use rand::rngs::SmallRng;
use rand::SeedableRng;
#[test]
fn reading_dwell_variant_serde_roundtrip() {
let w = WaitStrategy::ReadingDwell {
wpm: 250,
jitter_ms: 40,
};
let j = serde_json::to_string(&w).expect("serialize");
let back: WaitStrategy = serde_json::from_str(&j).expect("deserialize");
match back {
WaitStrategy::ReadingDwell { wpm, jitter_ms } => {
assert_eq!(wpm, 250);
assert_eq!(jitter_ms, 40);
}
other => panic!("expected ReadingDwell, got {other:?}"),
}
}
#[test]
fn reading_dwell_sleep_bounded() {
let mut rng = SmallRng::seed_from_u64(42);
let v = compute_dwell_ms(500, 250, 40, 500, 10_000, &mut rng);
assert_eq!(v, 10_000, "long reads must clamp to max");
let mut rng = SmallRng::seed_from_u64(7);
let v = compute_dwell_ms(0, 250, 40, 500, 10_000, &mut rng);
assert!(
(500..=10_000).contains(&v),
"zero-word base must still respect clamp, got {v}"
);
let mut a = SmallRng::seed_from_u64(123);
let mut b = SmallRng::seed_from_u64(123);
let va = compute_dwell_ms(80, 250, 40, 500, 10_000, &mut a);
let vb = compute_dwell_ms(80, 250, 40, 500, 10_000, &mut b);
assert_eq!(va, vb, "deterministic for a fixed seed");
let mut rng = SmallRng::seed_from_u64(999);
let v = compute_dwell_ms(20, 250, 40, 500, 10_000, &mut rng);
assert!(
(4_680..=4_920).contains(&v),
"jitter should stay within ~3σ of 4800 ms, got {v}"
);
}
}