captchaforge 0.2.39

Captcha detection and solving for Firefox and BiDi-driven browsers. Detection, vendor solver scaffolding, trusted cross-origin click delivery into nested OOPIFs, and stealth personas are implemented and tested; broad live-vendor solve rates are not yet benchmarked.
Documentation
//! Browser-level integration tests (no CAPTCHA logic, just BiDi / Firefox ops).

mod common;

#[tokio::test]
async fn browser_launches_and_closes() {
    let browser = common::launch_browser().await;
    browser.close().await.unwrap();
}

/// `drive_browser` is the production entry (CLI / examples / bindings). It now
/// launches via `launch_firefox_self_managed` (readiness-poll, no fixed-sleep
/// race) with the binary resolved from PATH, no `executable_path` is set here,
/// so this also proves the PATH-resolution fallback works for the prod path.
#[tokio::test]
async fn drive_browser_self_managed_path_resolves_and_runs() {
    use captchaforge::browser_runtime::{drive_browser, BrowserDriveOptions};
    let two: i32 = drive_browser(
        "about:blank",
        BrowserDriveOptions::default(),
        |page| async move {
            let v: i32 = page.evaluate("1 + 1").await?.into_value()?;
            Ok(v)
        },
    )
    .await
    .expect("drive_browser via self-managed PATH-resolved launch should work");
    assert_eq!(two, 2);
}

#[tokio::test]
async fn page_navigate_to_blank() {
    let browser = common::launch_browser().await;
    let page = browser.new_page("about:blank").await.unwrap();
    let url: String = page.url().await.unwrap_or_default();
    assert!(url.contains("about:blank"));
    browser.close().await.unwrap();
}

#[tokio::test]
async fn page_evaluate_returns_value() {
    let browser = common::launch_browser().await;
    let page = browser.new_page("about:blank").await.unwrap();
    let result: i32 = page.evaluate("1 + 1").await.unwrap().into_value().unwrap();
    assert_eq!(result, 2);
    browser.close().await.unwrap();
}

#[tokio::test]
async fn page_evaluate_sets_and_reads_title() {
    let browser = common::launch_browser().await;
    let page = browser.new_page("about:blank").await.unwrap();
    let _: String = page
        .evaluate("document.title = 'Test Title'; 'done'")
        .await
        .unwrap()
        .into_value()
        .unwrap();
    let title: String = page
        .evaluate("document.title")
        .await
        .unwrap()
        .into_value()
        .unwrap();
    assert_eq!(title, "Test Title");
    browser.close().await.unwrap();
}

#[tokio::test]
async fn page_evaluate_injects_dom() {
    let browser = common::launch_browser().await;
    let page = browser.new_page("about:blank").await.unwrap();
    let _: String = page
        .evaluate(r#"document.body.innerHTML = '<div id="box">hello</div>'; 'done'"#)
        .await
        .unwrap()
        .into_value()
        .unwrap();
    let text: String = page
        .evaluate("document.getElementById('box').innerText")
        .await
        .unwrap()
        .into_value()
        .unwrap();
    assert_eq!(text, "hello");
    browser.close().await.unwrap();
}

#[tokio::test]
async fn page_can_query_selector() {
    let browser = common::launch_browser().await;
    let page = browser.new_page("about:blank").await.unwrap();
    let _: String = page
        .evaluate(r#"document.body.innerHTML = '<button id="btn">Click</button>'; 'done'"#)
        .await
        .unwrap()
        .into_value()
        .unwrap();
    let exists: bool = page
        .evaluate("!!document.querySelector('#btn')")
        .await
        .unwrap()
        .into_value()
        .unwrap();
    assert!(exists);
    browser.close().await.unwrap();
}

#[tokio::test]
async fn local_server_serves_html() {
    let html = r#"<!DOCTYPE html><html><body><h1>Local</h1></body></html>"#.to_string();
    let addr = common::serve_once(html).await;
    let browser = common::launch_browser().await;
    let page = browser.new_page(&format!("http://{}", addr)).await.unwrap();
    let _title: String = page
        .evaluate("document.title")
        .await
        .unwrap()
        .into_value()
        .unwrap();
    // Title will be empty unless set, just verify page loaded.
    let body: String = page
        .evaluate("document.body.innerHTML")
        .await
        .unwrap()
        .into_value()
        .unwrap();
    assert!(body.contains("Local"));
    browser.close().await.unwrap();
}

#[tokio::test]
async fn multiple_browsers_are_isolated() {
    let b1 = common::launch_browser().await;
    let b2 = common::launch_browser().await;
    let p1 = b1.new_page("about:blank").await.unwrap();
    let p2 = b2.new_page("about:blank").await.unwrap();
    let _: String = p1
        .evaluate("document.title = 'Page1'; 'done'")
        .await
        .unwrap()
        .into_value()
        .unwrap();
    let _: String = p2
        .evaluate("document.title = 'Page2'; 'done'")
        .await
        .unwrap()
        .into_value()
        .unwrap();
    let t1: String = p1
        .evaluate("document.title")
        .await
        .unwrap()
        .into_value()
        .unwrap();
    let t2: String = p2
        .evaluate("document.title")
        .await
        .unwrap()
        .into_value()
        .unwrap();
    assert_eq!(t1, "Page1");
    assert_eq!(t2, "Page2");
    b1.close().await.unwrap();
    b2.close().await.unwrap();
}

#[tokio::test]
async fn page_js_error_is_caught() {
    let browser = common::launch_browser().await;
    let page = browser.new_page("about:blank").await.unwrap();
    let result = page.evaluate("throw new Error('boom')").await;
    assert!(result.is_err());
    browser.close().await.unwrap();
}