use car_browser::backend::BrowserBackend;
use car_browser::chromium::ChromiumBackend;
#[cfg(unix)]
fn pid_alive(pid: u32) -> bool {
unsafe { libc::kill(pid as libc::pid_t, 0) == 0 }
}
#[tokio::test]
#[ignore] async fn test_headless_launch_and_navigate() {
let backend = ChromiumBackend::launch()
.await
.expect("Failed to launch Chrome — is it installed?");
backend.navigate("https://example.com").await.unwrap();
let title = backend.get_page_title().await.unwrap();
assert!(
title.contains("Example"),
"Title should contain 'Example', got: {}",
title
);
let screenshot = backend.capture_screenshot().await.unwrap();
assert!(screenshot.len() > 1000, "Screenshot should be a real PNG");
assert_eq!(
&screenshot[..4],
&[0x89, 0x50, 0x4E, 0x47],
"Should start with PNG magic"
);
backend.shutdown().await.unwrap();
}
#[cfg(unix)]
#[tokio::test]
#[ignore] async fn test_headless_shutdown_terminates_chrome() {
let backend = ChromiumBackend::launch()
.await
.expect("Failed to launch Chrome — is it installed?");
let pid = backend.chrome_pid().expect("Chrome PID should be captured");
assert!(pid_alive(pid), "Chrome should be alive immediately after launch");
backend.shutdown().await.unwrap();
for _ in 0..50 {
if !pid_alive(pid) {
return;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
panic!("Chrome PID {} still alive 5s after shutdown()", pid);
}
#[cfg(unix)]
#[tokio::test]
#[ignore] async fn test_headless_drop_terminates_chrome() {
let pid = {
let backend = ChromiumBackend::launch()
.await
.expect("Failed to launch Chrome");
let pid = backend.chrome_pid().expect("Chrome PID should be captured");
assert!(pid_alive(pid), "Chrome should be alive after launch");
pid
};
for _ in 0..50 {
if !pid_alive(pid) {
return;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
panic!(
"Chrome PID {} still alive 5s after ChromiumBackend dropped without shutdown()",
pid
);
}
#[tokio::test]
#[ignore] async fn test_headless_accessibility_tree() {
let backend = ChromiumBackend::launch()
.await
.expect("Failed to launch Chrome");
backend.navigate("https://example.com").await.unwrap();
let nodes = backend.get_accessibility_tree().await.unwrap();
assert!(!nodes.is_empty(), "A11y tree should not be empty");
let has_link = nodes.iter().any(|n| n.role.contains("link"));
let has_heading = nodes.iter().any(|n| n.role.contains("heading"));
assert!(
has_link || has_heading,
"Should find links or headings on example.com. Roles found: {:?}",
nodes.iter().map(|n| &n.role).collect::<Vec<_>>()
);
backend.shutdown().await.unwrap();
}
#[tokio::test]
#[ignore] async fn test_headless_full_stack_with_car() {
use car_browser::perception::pipeline::BasicPerceptionPipeline;
use car_browser::perception::PerceptionPipeline;
use car_browser::BrowserToolExecutor;
use car_engine::{Runtime, ToolExecutor};
use car_ir::{Action, ActionProposal, ActionStatus, ActionType, FailureBehavior};
use serde_json::json;
use std::collections::HashMap;
use std::sync::Arc;
let backend = Arc::new(
ChromiumBackend::launch()
.await
.expect("Failed to launch Chrome"),
);
let pipeline: Arc<dyn PerceptionPipeline> = Arc::new(BasicPerceptionPipeline::new());
let executor = Arc::new(BrowserToolExecutor::new(
backend.clone() as Arc<dyn BrowserBackend>,
pipeline,
));
let rt = Runtime::new().with_executor(executor as Arc<dyn ToolExecutor>);
for schema in BrowserToolExecutor::tool_schemas() {
rt.register_tool_schema(schema).await;
}
let nav = ActionProposal {
id: "p1".into(),
source: "test".into(),
actions: vec![Action {
id: "a1".into(),
action_type: ActionType::ToolCall,
tool: Some("browse_navigate".into()),
parameters: {
let mut m = HashMap::new();
m.insert("url".into(), json!("https://example.com"));
m
},
preconditions: vec![],
expected_effects: HashMap::new(),
state_dependencies: vec![],
idempotent: false,
max_retries: 1,
failure_behavior: FailureBehavior::Abort,
timeout_ms: Some(30_000),
metadata: HashMap::new(),
}],
timestamp: chrono::Utc::now(),
context: HashMap::new(),
};
let result = rt.execute(&nav).await;
assert_eq!(result.results[0].status, ActionStatus::Succeeded);
let obs = ActionProposal {
id: "p2".into(),
source: "test".into(),
actions: vec![Action {
id: "a2".into(),
action_type: ActionType::ToolCall,
tool: Some("browse_observe".into()),
parameters: HashMap::new(),
preconditions: vec![],
expected_effects: HashMap::new(),
state_dependencies: vec![],
idempotent: true,
max_retries: 1,
failure_behavior: FailureBehavior::Abort,
timeout_ms: Some(30_000),
metadata: HashMap::new(),
}],
timestamp: chrono::Utc::now(),
context: HashMap::new(),
};
let result = rt.execute(&obs).await;
assert_eq!(result.results[0].status, ActionStatus::Succeeded);
let output = result.results[0].output.as_ref().unwrap();
let ui_map_text = output["ui_map"].as_str().unwrap();
assert!(
!ui_map_text.is_empty(),
"UiMap should have content from example.com"
);
assert!(
output["element_count"].as_u64().unwrap() > 0,
"Should detect elements on example.com"
);
println!("Headless UiMap:\n{}", ui_map_text);
println!("Elements: {}", output["element_count"]);
backend.shutdown().await.unwrap();
}