opencrabs 0.5.11

The autonomous, self-improving AI agent. Single Rust binary. Every channel. Recommended: the 40MB prebuilt binary for macOS, Linux and Windows: https://github.com/opencrabs/opencrabs/releases
//! Tests for the `browser_find` JS builder. Each test pins the shape
//! of the JS we send into the page — assigning a stable
//! `data-opencrabs-match` attribute and returning an array of
//! `{selector, text, tag, visible}` objects — so the selectors the
//! model passes back to browser_click are deterministic.
//!
//! We can't run the JS (that requires a real page / V8), so these
//! tests verify we emit the right script shape for each mode and
//! that user-supplied patterns are correctly escaped.

#![cfg(feature = "browser")]

use crate::brain::tools::browser::build_find_js;

#[test]
fn css_mode_uses_deep_query_selector_all() {
    let js = build_find_js("css", "button.primary", 20);
    // `__ocQueryAll` instead of `document.querySelectorAll`: css search
    // reaches inside open shadow roots, and the limit is applied by the
    // helper rather than a trailing `.slice`.
    assert!(js.contains(r#"__ocQueryAll("button.primary", 20)"#));
}

#[test]
fn xpath_mode_uses_document_evaluate() {
    let js = build_find_js("xpath", "//button[@type='submit']", 5);
    assert!(js.contains("document.evaluate("));
    assert!(js.contains("XPathResult.ORDERED_NODE_SNAPSHOT_TYPE"));
    assert!(js.contains("//button[@type='submit']"));
    assert!(js.contains("i < 5"));
}

#[test]
fn text_mode_walks_dom_for_substring() {
    let js = build_find_js("text", "Sign in", 10);
    // The composed-tree walk replaces the flat
    // `createTreeWalker(document.body, ...)`, so text search sees
    // elements rendered inside open shadow roots.
    assert!(js.contains("for (const node of __ocWalk())"));
    assert!(js.contains("out.length >= 10"));
    // Pattern gets lowercased server-side for case-insensitive match
    assert!(js.contains(r#""Sign in".toLowerCase()"#));
}

#[test]
fn aria_mode_uses_attribute_selector() {
    let js = build_find_js("aria", "Close dialog", 3);
    assert!(js.contains(r#"[aria-label*="Close dialog" i]"#));
}

#[test]
fn unknown_mode_defaults_to_css() {
    let js = build_find_js("nonsense", ".btn", 5);
    assert!(
        js.contains(r#"__ocQueryAll(".btn", 5)"#),
        "unknown mode must fall back to CSS selector path"
    );
}

#[test]
fn escapes_double_quotes_in_pattern() {
    // A pattern containing a double quote would otherwise close the JS
    // string literal early and inject arbitrary code into the page
    // context. This guard is a security boundary.
    let js = build_find_js("css", "div[data-foo=\"bar\"]", 5);
    // The inner double quotes must be backslash-escaped so the
    // outer querySelectorAll(" … ") stays balanced.
    assert!(js.contains(r#"div[data-foo=\"bar\"]"#));
    // And no raw unescaped `"bar"` substring sneaks through breaking
    // the outer literal.
    assert!(
        !js.contains(r#"__ocQueryAll("div[data-foo="bar"]", 5)"#),
        "unescaped inner double-quotes would break the JS string literal"
    );
}

#[test]
fn escapes_backslashes() {
    // Backslash is the JS escape introducer — leaving one raw lets the
    // user's pattern bend subsequent chars into escape sequences.
    let js = build_find_js("css", r"div\.class", 5);
    assert!(js.contains(r"div\\.class"));
}

#[test]
fn clears_previous_match_attributes_before_re_enumerating() {
    // Every call starts by stripping any `data-opencrabs-match`
    // attributes left over from the previous call. Without this,
    // stale indices from call N would coexist with fresh indices
    // from call N+1 on different elements and the returned selector
    // would be ambiguous.
    // The clear is DEEP (`__ocClearStamps`): once we stamp inside a
    // shadow root, a flat `document.querySelectorAll` cleanup would
    // leave those stamps rotting and a stale
    // `[data-opencrabs-match="3"]` could resolve to a node from a
    // previous page state.
    let js = build_find_js("css", ".btn", 5);
    assert!(js.contains("__ocClearStamps();"));
    assert!(js.contains("removeAttribute('data-opencrabs-match')"));
}

#[test]
fn xpath_mode_stays_light_dom_by_spec() {
    // XPath has no notion of a shadow boundary, so `document.evaluate`
    // cannot cross one however we call it. The mode must therefore keep
    // the flat evaluate and the tool description must say so.
    let js = build_find_js("xpath", "//button", 5);
    assert!(js.contains("document.evaluate("));
    // Scope the negative to the node-collection expression: the shared
    // wrapper DEFINES the deep helpers for every mode, so asserting
    // against the whole script would only prove the preamble exists.
    let collector = js
        .split("const nodes =")
        .nth(1)
        .and_then(|rest| rest.split("const out = [];").next())
        .expect("wrapper splices the collector in as `const nodes = ...`");
    assert!(!collector.contains("__ocQueryAll("));
    assert!(!collector.contains("__ocWalk()"));
}

#[test]
fn returns_object_with_stable_selector_per_match() {
    // The payload shape the model sees must be:
    //   { selector, text, tag, visible, shadow }
    // Selector uses the attribute we just assigned so it's unique
    // and survives subsequent DOM churn (within the same turn).
    let js = build_find_js("css", "button", 5);
    assert!(js.contains(r#"selector: '[data-opencrabs-match="' + i + '"]'"#));
    assert!(js.contains("text:"));
    assert!(js.contains("tag:"));
    assert!(js.contains("visible:"));
    // `shadow` is additive — it tells the model the element sits behind
    // a shadow boundary without changing the selector shape.
    assert!(js.contains("shadow: __ocInShadow(el)"));
}