use browser_oxide::stealth::presets::chrome_148_macos;
use browser_oxide::Page;
const HTML: &str = r#"<!doctype html><html><head><title>humanize</title></head><body>
<script>
globalThis.__events = [];
globalThis.__lastMove = null;
const record = (kind, ev) => {
const e = { kind, t: Date.now() };
if (ev) {
if (typeof ev.clientX === 'number') { e.x = ev.clientX; e.y = ev.clientY; }
if (typeof ev.deltaY === 'number') e.deltaY = ev.deltaY;
if (typeof ev.key === 'string') e.key = ev.key;
e.isTrusted = ev.isTrusted;
}
globalThis.__events.push(e);
};
document.addEventListener('mousemove', e => record('mousemove', e));
document.addEventListener('mousedown', e => record('mousedown', e));
document.addEventListener('mouseup', e => record('mouseup', e));
document.addEventListener('click', e => record('click', e));
document.addEventListener('wheel', e => record('wheel', e), { passive: true });
document.addEventListener('scroll', e => record('scroll', e), { passive: true });
document.addEventListener('keydown', e => record('keydown', e));
document.addEventListener('keyup', e => record('keyup', e));
document.addEventListener('visibilitychange', e => record('visibilitychange', e));
window.addEventListener('focus', e => record('focus', e));
window.addEventListener('scroll', e => record('scroll-win', e));
</script>
</body></html>"#;
#[tokio::test]
#[ignore = "not yet implemented: wheel-event emission in the synthetic input path"]
async fn humanize_emits_full_signal_set() {
let mut page = Page::from_html(HTML, Some(chrome_148_macos()))
.await
.unwrap();
let humanize = include_str!("../src/js/humanize.js");
page.evaluate(humanize).unwrap();
let _ = page
.event_loop()
.run_until_idle(std::time::Duration::from_secs(4))
.await;
let n_total = page.evaluate("globalThis.__events.length").unwrap();
let n_mousemove = page
.evaluate("globalThis.__events.filter(e => e.kind === 'mousemove').length")
.unwrap();
let n_wheel = page
.evaluate("globalThis.__events.filter(e => e.kind === 'wheel').length")
.unwrap();
let n_click = page
.evaluate("globalThis.__events.filter(e => e.kind === 'click').length")
.unwrap();
let n_keydown = page
.evaluate("globalThis.__events.filter(e => e.kind === 'keydown').length")
.unwrap();
let total_distance = page
.evaluate(
"(() => {\
const m = globalThis.__events.filter(e => e.kind === 'mousemove' && typeof e.x === 'number');\
let d = 0;\
for (let i = 1; i < m.length; i++) { d += Math.hypot(m[i].x-m[i-1].x, m[i].y-m[i-1].y); }\
return Math.round(d);\
})()",
)
.unwrap();
let monotonic_decel = page
.evaluate(
"(() => {\
const w = globalThis.__events.filter(e => e.kind === 'wheel');\
if (w.length < 2) return 'too-few';\
let prev = Infinity;\
for (const e of w) { if (e.deltaY > prev) return 'not-monotonic'; prev = e.deltaY; }\
return 'ok';\
})()",
)
.unwrap();
let trusted_count = page
.evaluate("globalThis.__events.filter(e => e.isTrusted === true).length")
.unwrap();
println!("\n=== Humanize signal probe ===");
println!(" total events: {n_total}");
println!(" mousemove: {n_mousemove}");
println!(" wheel: {n_wheel} (decel: {monotonic_decel})");
println!(" click: {n_click}");
println!(" keydown: {n_keydown}");
println!(" path distance: {total_distance} px");
println!(" isTrusted=true: {trusted_count}");
let mm = n_mousemove.parse::<i64>().unwrap();
assert!(
mm >= 30,
"humanize must emit ≥30 mousemove events, got {mm}"
);
let w = n_wheel.parse::<i64>().unwrap();
assert!(w >= 4, "humanize must emit ≥4 wheel events, got {w}");
let c = n_click.parse::<i64>().unwrap();
assert!(c >= 1, "humanize must emit ≥1 click");
let k = n_keydown.parse::<i64>().unwrap();
assert!(k >= 1, "humanize must emit ≥1 keydown");
let dist = total_distance.parse::<i64>().unwrap();
assert!(
dist >= 800,
"mouse path must cover ≥800 px (sensors check magnitude), got {dist}"
);
assert_eq!(
monotonic_decel.trim_matches('"'),
"ok",
"scroll wheel deltaY must monotonically decrease (deceleration model)"
);
let trusted = trusted_count.parse::<i64>().unwrap();
assert!(
trusted >= 30,
"events must have isTrusted=true so handlers gating on it fire — got {trusted}"
);
}
#[tokio::test]
async fn humanize_mouse_intervals_are_right_skewed() {
let mut page = Page::from_html(HTML, Some(chrome_148_macos()))
.await
.unwrap();
let humanize = include_str!("../src/js/humanize.js");
page.evaluate(humanize).unwrap();
for _ in 0..90 {
let _ = page
.event_loop()
.run_until_idle(std::time::Duration::from_millis(20))
.await;
tokio::time::sleep(std::time::Duration::from_millis(40)).await;
}
let stats = page.evaluate(
"(() => {\
const m = globalThis.__events.filter(e => e.kind === 'mousemove' && typeof e.t === 'number');\
if (m.length < 10) return JSON.stringify({error: 'too-few', n: m.length});\
const intervals = [];\
for (let i = 1; i < m.length; i++) intervals.push(m[i].t - m[i-1].t);\
intervals.sort((a, b) => a - b);\
const mid = intervals.length >> 1;\
const median = intervals.length % 2 ? intervals[mid] : (intervals[mid-1]+intervals[mid])/2;\
const mean = intervals.reduce((a, b) => a + b, 0) / intervals.length;\
const minVal = intervals[0];\
const maxVal = intervals[intervals.length - 1];\
return JSON.stringify({n: intervals.length, mean: +mean.toFixed(2), median: +median.toFixed(2), min: +minVal.toFixed(2), max: +maxVal.toFixed(2)});\
})()"
).unwrap();
println!("\nmousemove inter-arrival stats: {}", stats);
let stats_clean = stats.trim_matches('"').replace("\\\"", "\"");
let v: serde_json::Value = serde_json::from_str(&stats_clean).expect("stats json");
let mean = v["mean"].as_f64().unwrap();
let median = v["median"].as_f64().unwrap();
let max = v["max"].as_f64().unwrap();
let min = v["min"].as_f64().unwrap();
assert!(
mean > median,
"right-skewed distribution must have mean > median (sigma-lognormal property), got mean={mean} median={median}"
);
assert!(
max > min * 3.0,
"interval range must span ≥3× (asymmetric distribution; uniform sampling would have max ≈ min), got min={min} max={max}"
);
}