use std::time::Duration;
use captchaforge::detect::{CaptchaInfo, DetectedCaptcha};
use captchaforge::solver::{CaptchaSolver, SolveConfig, TurnstileInteractiveSolver};
use guise::browser::launch_reynard;
use guise::fingerprint::StealthProfile;
mod common;
const PAGE: &str = r#"<!doctype html><html><head><meta charset="utf-8"><title>ts-interactive</title>
<script>
window.onloadTurnstileCallback = function () {
try {
window.turnstile.render('#ts', { sitekey: '3x00000000000000000000FF' });
} catch (e) { var d=document.getElementById('err'); if(d) d.textContent='render-err:'+e; }
};
</script>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback" async defer></script>
</head><body>
<h1>Turnstile interactive test</h1>
<form id="f"><div id="ts" class="cf-turnstile"></div></form>
<div id="err"></div>
</body></html>"#;
const TOKEN_LEN_JS: &str = r#"(function(){var e=document.querySelector('input[name=cf-turnstile-response]');return e?(e.value||'').length:0;})()"#;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn reynard_solves_force_interactive_turnstile() {
let Ok(reynard_bin) = std::env::var("REYNARD_BIN") else {
eprintln!("SKIP turnstile_interactive_live: set REYNARD_BIN to run");
return;
};
if std::env::var("DISPLAY").is_err() {
eprintln!("SKIP turnstile_interactive_live: no DISPLAY");
return;
}
let addr = common::serve_once(PAGE.to_string()).await;
let url = format!("http://{addr}/");
let page = launch_reynard(&reynard_bin, &StealthProfile::FirefoxLinux, false)
.await
.expect("launch reynard");
page.goto(&url)
.await
.expect("navigate to local turnstile page");
tokio::time::sleep(Duration::from_millis(3500)).await;
let dom = page
.evaluate(
r#"(function(){
var ifr=document.querySelectorAll('iframe');
var srcs=[];for(var i=0;i<ifr.length;i++){srcs.push((ifr[i].src||'').slice(0,60));}
return JSON.stringify({
turnstileGlobal: typeof window.turnstile,
iframeCount: ifr.length,
iframeSrcs: srcs,
cfDiv: !!document.querySelector('.cf-turnstile'),
isSecure: window.isSecureContext,
bodyLen: document.body?document.body.innerText.length:0
});
})()"#,
)
.await
.ok()
.and_then(|e| e.into_value::<String>().ok())
.unwrap_or_default();
eprintln!("[ts-interactive] DOM after load: {dom}");
let iframe_count = page
.evaluate("document.querySelectorAll('iframe').length")
.await
.ok()
.and_then(|e| e.into_value::<u64>().ok())
.unwrap_or(0);
if iframe_count == 0 {
eprintln!(
"SKIP turnstile_interactive_live: the 3x…FF force-interactive TEST key presents \
no challenge iframe on a self-hosted origin in any Firefox (stock == reynard, \
proven by bench turnstile_interactive_reynard), no checkbox exists to click. \
Mechanical click path is proven by foxdriver cross_origin_click. DOM: {dom}"
);
let _ = page.close().await;
return;
}
let info = CaptchaInfo {
kind: DetectedCaptcha::Turnstile,
site_key: Some("3x00000000000000000000FF".into()),
page_url: url.clone(),
container_selector: Some(".cf-turnstile".into()),
};
let solver = TurnstileInteractiveSolver::new().with_config(SolveConfig {
checkbox_max_attempts: 12,
token_max_attempts: 20,
..SolveConfig::default()
});
let res = solver.solve(&page, &info).await;
let token_len = page
.evaluate(TOKEN_LEN_JS)
.await
.ok()
.and_then(|e| e.into_value::<u64>().ok())
.unwrap_or(0);
let _ = page.close().await;
let res = res.expect("solver returned an error");
eprintln!(
"[ts-interactive] success={} solution={:?} token_len={}",
res.success, res.solution, token_len
);
assert!(
res.success || token_len > 0,
"TurnstileInteractiveSolver did NOT solve the force-interactive widget \
(success={}, token_len={}), the cross-origin checkbox click likely did \
not reach the OOPIF",
res.success,
token_len
);
}