use crate::Page;
use anyhow::Result;
use async_trait::async_trait;
use super::{parse_detection_result, CaptchaInfo, Detector};
pub struct ImageCaptchaDetector;
impl ImageCaptchaDetector {
pub const JS: &'static str = r#"(function() {
const imgCaptcha = document.querySelector(
'img[src*="captcha"], img[src*="Captcha"], .captcha img, #captcha img'
);
if (imgCaptcha) {
return { kind: 'image', site_key: null, container: '.captcha, #captcha' };
}
/* Tile-grid widgets (icon picker, color picker, image grid). The
VLM grid-click branch needs Image kind, not Canvas. We
disambiguate by requiring 4+ tile children, the giveaway for a
grid challenge vs a single-image captcha. */
const gridSelectors = [
'.icon-grid', '.color-grid', '.captcha-grid',
'[class*="icon-captcha-grid"]', '[class*="image-grid"]'
];
for (const sel of gridSelectors) {
const grid = document.querySelector(sel);
if (grid && grid.children && grid.children.length >= 4) {
return { kind: 'image', site_key: null, container: sel };
}
}
return { kind: 'none', site_key: null, container: null };
})()"#;
}
#[async_trait]
impl Detector for ImageCaptchaDetector {
fn name(&self) -> &'static str {
"image_captcha"
}
fn priority(&self) -> i32 {
40
}
async fn detect(&self, page: &Page) -> Result<Option<CaptchaInfo>> {
let raw = page.evaluate(Self::JS).await?;
let val = raw.into_value::<serde_json::Value>()?;
Ok(parse_detection_result(val))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn image_captcha_detector_js_has_selectors() {
assert!(ImageCaptchaDetector::JS.contains("img[src*=\"captcha\"]"));
assert!(ImageCaptchaDetector::JS.contains(".captcha img"));
}
#[test]
fn image_captcha_detector_js_handles_tile_grids() {
let js = ImageCaptchaDetector::JS;
assert!(js.contains(".icon-grid"));
assert!(js.contains(".color-grid"));
assert!(js.contains(".captcha-grid"));
assert!(js.contains("children.length >= 4"));
}
}