mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 07:30:11 +00:00
- 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]>
204 lines
6.6 KiB
Rust
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!");
|
|
}
|