Files
optimclaw/tests/browser_integration.rs
T
Illia PolosukhinandClaude Opus 4.6 cde50ff470 fix: Harden browser tool against selector injection and cross-platform issues
- Use serde_json::to_string() for CSS selector escaping in extract_text()
  and wait() instead of naive single-quote replacement, preventing JS
  injection via crafted selectors
- Match AxPropertyName enum variants directly instead of fragile
  Debug-format substring matching in node_has_property()
- Remove dead node_by_id HashMap construction and unused guess_selector
  parameter in accessibility tree builder
- Use platform-aware PATH separator (';' on Windows, ':' elsewhere) in
  which_chrome_in_path()

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-02-15 00:07:35 -08:00

204 lines
6.6 KiB
Rust

//! Integration test for the browser tool.
//!
//! Requires Chrome installed. Run with:
//! cargo test --test browser_integration -- --nocapture
use ironclaw::context::JobContext;
use ironclaw::tools::Tool;
use ironclaw::tools::builtin::{BrowserTool, find_chrome};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_browser_navigate_and_screenshot() {
// Skip if Chrome/Chromium is not installed (works on macOS, Linux, Windows).
if find_chrome().is_none() {
eprintln!("Skipping: Chrome not found");
return;
}
let tool = BrowserTool::new();
let ctx = JobContext::default();
// 1. Navigate to Wikipedia
eprintln!("=== Navigating to Wikipedia...");
let nav_result = tool
.execute(
serde_json::json!({
"action": "navigate",
"url": "https://en.wikipedia.org/wiki/Mariam_Almheiri"
}),
&ctx,
)
.await;
match &nav_result {
Ok(output) => {
eprintln!(
"Navigation result: {}",
serde_json::to_string_pretty(&output.result).unwrap()
);
let title = output
.result
.get("title")
.and_then(|t| t.as_str())
.unwrap_or("");
assert!(
title.contains("Mariam") || title.contains("Almheiri"),
"Page title should mention Mariam Almheiri, got: {}",
title
);
}
Err(e) => {
eprintln!("Navigation failed: {}", e);
panic!("Navigation should succeed");
}
}
// 2. Read the accessibility tree
eprintln!("\n=== Reading page accessibility tree...");
let read_result = tool
.execute(serde_json::json!({"action": "read_page"}), &ctx)
.await;
match &read_result {
Ok(output) => {
let tree = output.result.as_str().unwrap_or("");
let line_count = tree.lines().count();
eprintln!("Accessibility tree: {} lines", line_count);
// Print first 20 lines
for line in tree.lines().take(20) {
eprintln!(" {}", line);
}
if line_count > 20 {
eprintln!(" ... ({} more lines)", line_count - 20);
}
assert!(line_count > 3, "Should have some elements on the page");
}
Err(e) => {
eprintln!("Read page failed: {}", e);
panic!("Read page should succeed");
}
}
// 3. Get page dimensions via eval_js to compute center
eprintln!("\n=== Getting page dimensions...");
let dims_result = tool
.execute(
serde_json::json!({
"action": "eval_js",
"expression": "JSON.stringify({w: window.innerWidth, h: window.innerHeight, scrollH: document.body.scrollHeight})"
}),
&ctx,
)
.await;
let (viewport_w, viewport_h) = match &dims_result {
Ok(output) => {
let result_str = output
.result
.get("result")
.and_then(|r| r.as_str())
.unwrap_or("{}");
let dims: serde_json::Value = serde_json::from_str(result_str).unwrap_or_default();
let w = dims.get("w").and_then(|v| v.as_f64()).unwrap_or(1920.0);
let h = dims.get("h").and_then(|v| v.as_f64()).unwrap_or(1080.0);
eprintln!("Viewport: {}x{}", w, h);
(w, h)
}
Err(e) => {
eprintln!("eval_js failed: {}", e);
(1920.0, 1080.0)
}
};
// 4. Scroll to middle of page first
eprintln!("\n=== Scrolling to middle of page...");
let _ = tool
.execute(
serde_json::json!({
"action": "eval_js",
"expression": "window.scrollTo(0, document.body.scrollHeight / 2 - window.innerHeight / 2)"
}),
&ctx,
)
.await;
// Brief wait for scroll to settle
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
// 5. Take full viewport screenshot
eprintln!("\n=== Taking viewport screenshot...");
let screenshot_result = tool
.execute(serde_json::json!({"action": "screenshot"}), &ctx)
.await;
match &screenshot_result {
Ok(output) => {
let b64 = output
.result
.get("data")
.and_then(|d| d.as_str())
.unwrap_or("");
eprintln!(
"Screenshot: {} base64 chars ({} bytes decoded)",
b64.len(),
b64.len() * 3 / 4
);
// Save to /tmp for inspection
use base64::Engine;
if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(b64) {
let path = "/tmp/ironclaw_browser_test_viewport.png";
if std::fs::write(path, &bytes).is_ok() {
eprintln!("Saved viewport screenshot to {}", path);
}
// Now crop the center 10x10 using raw PNG manipulation
// We'll use eval_js to take a clipped screenshot via CDP directly
}
}
Err(e) => {
eprintln!("Screenshot failed: {}", e);
panic!("Screenshot should succeed");
}
}
// 6. Take a 10x10 screenshot from the center of the viewport using eval_js
// We can't directly use the clip param through the current tool API,
// so we'll take the viewport screenshot and note the center crop coords.
let center_x = (viewport_w / 2.0 - 5.0).max(0.0);
let center_y = (viewport_h / 2.0 - 5.0).max(0.0);
eprintln!(
"\n=== Center 10x10 crop would be at ({}, {}) to ({}, {})",
center_x,
center_y,
center_x + 10.0,
center_y + 10.0
);
// 7. Extract some text to verify content loaded
eprintln!("\n=== Extracting page text...");
let extract_result = tool
.execute(
serde_json::json!({"action": "extract", "selector": "h1"}),
&ctx,
)
.await;
match &extract_result {
Ok(output) => {
let text = output.result.as_str().unwrap_or("");
eprintln!("H1 text: {}", text);
assert!(
text.contains("Mariam") || text.contains("Almheiri"),
"H1 should contain the article subject, got: {}",
text
);
}
Err(e) => {
eprintln!("Extract failed: {}", e);
}
}
eprintln!("\n=== All browser integration tests passed!");
}