use captchaforge::detect::{CaptchaInfo, DetectedCaptcha};
use captchaforge::solver::{CaptchaSolver, MultiStepCaptchaSolver};
mod common;
const WIZARD: &str = r#"<!doctype html><html><head><meta charset="utf-8"><title>wizard</title>
<style>.step{display:none}.step.active{display:block}</style></head><body>
<div class="step active" id="step1">
<p>Solve: 3 + 4</p>
<input id="ans1" type="number">
<button id="next1">Next</button>
</div>
<div class="step" id="step2">
<p>One more thing</p>
<button id="verify2">Verify</button>
</div>
<script>
document.getElementById('next1').addEventListener('click', function(e){
if (e.isTrusted && document.getElementById('ans1').value === '7') {
document.getElementById('step1').classList.remove('active');
document.getElementById('step2').classList.add('active');
}
});
document.getElementById('verify2').addEventListener('click', function(e){
if (e.isTrusted) {
document.getElementById('step2').classList.remove('active');
document.title = 'Solved';
}
});
</script></body></html>"#;
#[tokio::test]
async fn multi_step_solver_advances_a_trusted_click_gated_wizard() {
if which::which("firefox").is_err() {
eprintln!("SKIP multi_step_trusted_click: firefox not on PATH");
return;
}
let addr = common::serve_once(WIZARD.to_string()).await;
let url = format!("http://{addr}/");
let browser = common::launch_browser().await;
let page = browser.new_page(&url).await.expect("navigate to wizard");
let after_untrusted = page
.evaluate(
r#"(() => {
document.getElementById('ans1').value = '7';
document.getElementById('next1').click();
const a = document.querySelector('.step.active');
return a ? a.id : '';
})()"#,
)
.await
.expect("untrusted-click control eval")
.into_value::<String>()
.unwrap_or_default();
assert_eq!(
after_untrusted, "step1",
"fixture sanity: an untrusted JS click must NOT advance the wizard (it stayed on \
{after_untrusted:?} instead of step1), otherwise the test cannot prove trusted delivery"
);
let info = CaptchaInfo {
kind: DetectedCaptcha::MultiStepCaptcha,
site_key: None,
page_url: url.clone(),
container_selector: Some(".step.active".into()),
};
let res = MultiStepCaptchaSolver::new()
.solve(&page, &info)
.await
.expect("solver returned an error");
let title = page
.evaluate("document.title")
.await
.ok()
.and_then(|e| e.into_value::<String>().ok())
.unwrap_or_default();
let _ = browser.close().await;
assert!(
res.success,
"MultiStepCaptchaSolver must report success on the trusted-click-gated wizard \
(got success={}, solution={:?}), a JS el.click() (isTrusted=false) would be \
rejected by every step and never reach 'Solved'",
res.success, res.solution
);
assert_eq!(
title, "Solved",
"the wizard's title must flip to 'Solved', only trusted clicks on Next + Verify \
advance it; got title={title:?}"
);
}