use captchaforge::captcha_detect::{CaptchaInfo, DetectedCaptcha};
use captchaforge::solver::{BehavioralCaptchaSolver, CaptchaSolver};
mod common;
async fn clicks(page: &captchaforge::Page) -> Vec<(String, bool)> {
let raw = page
.evaluate("JSON.stringify(window.__clicks || [])")
.await
.ok()
.and_then(|e| e.into_value::<String>().ok())
.unwrap_or_default();
serde_json::from_str::<Vec<serde_json::Value>>(&raw)
.unwrap_or_default()
.into_iter()
.filter_map(|v| Some((v["id"].as_str()?.to_string(), v["trusted"].as_bool().unwrap_or(false))))
.collect()
}
const RECORDER: &str = r#"<script>
window.__clicks = [];
window.__verifyTrusted = null;
document.querySelectorAll('.cell, .icon-item, .color-item').forEach(function(el){
el.addEventListener('click', function(e){ window.__clicks.push({ id: el.id, trusted: e.isTrusted }); });
});
var vb = document.querySelector('#recaptcha-verify-button, #submit');
if (vb) vb.addEventListener('click', function(e){ window.__verifyTrusted = e.isTrusted; });
</script>"#;
async fn assert_grid_trusted(body_html: &str, want: &[&str], never: &[&str]) {
let html = format!(
"<!doctype html><html><head><meta charset=\"utf-8\"><title>grid</title></head><body>{body_html}{RECORDER}</body></html>"
);
let browser = common::launch_browser().await;
let addr = common::serve_once(html).await;
let url = format!("http://{addr}/");
let page = browser.new_page(&url).await.expect("navigate");
let info = CaptchaInfo {
kind: DetectedCaptcha::ImageCaptcha,
site_key: None,
page_url: url.clone(),
container_selector: Some("#widget".into()),
};
let _ = BehavioralCaptchaSolver::new().solve(page, &info).await;
let recorded = clicks(page).await;
for w in want {
assert!(
recorded.iter().any(|(id, _)| id == w),
"tile {w} must be clicked; recorded={recorded:?}"
);
}
for n in never {
assert!(
!recorded.iter().any(|(id, _)| id == n),
"non-matching tile {n} must NOT be clicked; recorded={recorded:?}"
);
}
for (id, trusted) in &recorded {
assert!(
*trusted,
"tile {id} click must be isTrusted === true (a same-origin captcha can \
gate on it); recorded={recorded:?}"
);
}
let verify_trusted = page
.evaluate("window.__verifyTrusted")
.await
.ok()
.and_then(|e| e.into_value::<Option<bool>>().ok())
.flatten();
assert_eq!(verify_trusted, Some(true), "verify button click must be isTrusted === true");
let _ = browser.close().await;
}
fn cell(id: &str, emoji: &str) -> String {
format!(r#"<div class="cell" id="{id}" style="display:inline-block;width:64px;height:64px">{emoji}</div>"#)
}
fn icon(id: &str, emoji: &str) -> String {
format!(r#"<div class="icon-item" id="{id}" style="display:inline-block;width:64px;height:64px">{emoji}</div>"#)
}
fn color(id: &str, rgb: &str) -> String {
format!(r#"<div class="color-item" id="{id}" style="display:inline-block;width:64px;height:64px;background-color:{rgb}"></div>"#)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn behavioral_image_grid_clicks_correct_tiles_trusted() {
let cells = [
cell("c0", "🚦"), cell("c1", "🌳"), cell("c2", "🚦"),
cell("c3", "🍎"), cell("c4", "🚦"), cell("c5", "🐶"),
cell("c6", "🚗"), cell("c7", "🏠"), cell("c8", "🚦"),
]
.concat();
let body = format!(
r#"<div id="widget"><div class="prompt">Select all traffic lights</div>
<div class="grid">{cells}</div>
<button id="recaptcha-verify-button">Verify</button></div>"#
);
assert_grid_trusted(&body, &["c0", "c2", "c4", "c8"], &["c1", "c3", "c5", "c6", "c7"]).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn behavioral_icon_grid_clicks_correct_tiles_trusted() {
let icons = [
icon("i0", "🚗"), icon("i1", "🐶"), icon("i2", "🚌"),
icon("i3", "🍎"), icon("i4", "🏍"), icon("i5", "🌳"),
]
.concat();
let body = format!(
r#"<div id="widget"><div class="prompt">Select all vehicles</div>
<div class="grid">{icons}</div>
<button id="submit">Verify</button></div>"#
);
assert_grid_trusted(&body, &["i0", "i2", "i4"], &["i1", "i3", "i5"]).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn behavioral_color_grid_clicks_correct_tiles_trusted() {
let tiles = [
color("k0", "rgb(220,20,20)"), color("k1", "rgb(20,120,220)"), color("k2", "rgb(220,30,30)"), color("k3", "rgb(30,160,40)"), color("k4", "rgb(200,20,20)"), color("k5", "rgb(240,220,40)"), ]
.concat();
let body = format!(
r#"<div id="widget"><div class="prompt">Select all red tiles</div>
<div class="grid">{tiles}</div>
<button id="submit">Verify</button></div>"#
);
assert_grid_trusted(&body, &["k0", "k2", "k4"], &["k1", "k3", "k5"]).await;
}