use serde_json::{json, Value};
use super::actions::{execute_command, DaemonState};
fn assert_success(resp: &Value) {
assert_eq!(
resp.get("success").and_then(|v| v.as_bool()),
Some(true),
"Expected success but got: {}",
serde_json::to_string_pretty(resp).unwrap_or_default()
);
}
fn get_data(resp: &Value) -> &Value {
resp.get("data").expect("Missing 'data' in response")
}
#[tokio::test]
#[ignore]
async fn e2e_launch_navigate_evaluate_close() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["launched"], true);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "https://example.com" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["url"], "https://example.com/");
assert_eq!(get_data(&resp)["title"], "Example Domain");
let resp = execute_command(&json!({ "id": "3", "action": "url" }), &mut state).await;
assert_success(&resp);
assert_eq!(get_data(&resp)["url"], "https://example.com/");
let resp = execute_command(&json!({ "id": "4", "action": "title" }), &mut state).await;
assert_success(&resp);
assert_eq!(get_data(&resp)["title"], "Example Domain");
let resp = execute_command(
&json!({ "id": "5", "action": "evaluate", "script": "1 + 2" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["result"], 3);
let resp = execute_command(
&json!({ "id": "6", "action": "evaluate", "script": "document.title" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["result"], "Example Domain");
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
assert_eq!(get_data(&resp)["closed"], true);
}
#[tokio::test]
#[ignore]
async fn e2e_lightpanda_launch_can_open_page() {
let lightpanda_bin = match std::env::var("LIGHTPANDA_BIN") {
Ok(path) if !path.is_empty() => path,
_ => return,
};
let mut state = DaemonState::new();
let resp = tokio::time::timeout(
tokio::time::Duration::from_secs(20),
execute_command(
&json!({
"id": "1",
"action": "launch",
"headless": true,
"engine": "lightpanda",
"executablePath": lightpanda_bin,
}),
&mut state,
),
)
.await
.expect("Lightpanda launch should not hang");
assert_success(&resp);
assert_eq!(get_data(&resp)["launched"], true);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "https://example.com" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["url"], "https://example.com/");
assert_eq!(get_data(&resp)["title"], "Example Domain");
let resp = execute_command(&json!({ "id": "3", "action": "close" }), &mut state).await;
assert_success(&resp);
assert_eq!(get_data(&resp)["closed"], true);
}
#[tokio::test]
#[ignore]
async fn e2e_lightpanda_auto_launch_can_open_page() {
let lightpanda_bin = match std::env::var("LIGHTPANDA_BIN") {
Ok(path) if !path.is_empty() => path,
_ => return,
};
let prev_engine = std::env::var("AGENT_BROWSER_ENGINE").ok();
let prev_path = std::env::var("AGENT_BROWSER_EXECUTABLE_PATH").ok();
std::env::set_var("AGENT_BROWSER_ENGINE", "lightpanda");
std::env::set_var("AGENT_BROWSER_EXECUTABLE_PATH", &lightpanda_bin);
let mut state = DaemonState::new();
let resp = tokio::time::timeout(
tokio::time::Duration::from_secs(20),
execute_command(
&json!({ "id": "1", "action": "navigate", "url": "https://example.com" }),
&mut state,
),
)
.await
.expect("Lightpanda auto-launch should not hang");
match prev_engine {
Some(value) => std::env::set_var("AGENT_BROWSER_ENGINE", value),
None => std::env::remove_var("AGENT_BROWSER_ENGINE"),
}
match prev_path {
Some(value) => std::env::set_var("AGENT_BROWSER_EXECUTABLE_PATH", value),
None => std::env::remove_var("AGENT_BROWSER_EXECUTABLE_PATH"),
}
assert_success(&resp);
assert_eq!(get_data(&resp)["url"], "https://example.com/");
assert_eq!(get_data(&resp)["title"], "Example Domain");
let resp = execute_command(&json!({ "id": "2", "action": "close" }), &mut state).await;
assert_success(&resp);
assert_eq!(get_data(&resp)["closed"], true);
}
#[tokio::test]
#[ignore]
async fn e2e_snapshot_and_click_ref() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "https://example.com" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(&json!({ "id": "3", "action": "snapshot" }), &mut state).await;
assert_success(&resp);
let snapshot = get_data(&resp)["snapshot"].as_str().unwrap();
assert!(
snapshot.contains("Example Domain"),
"Snapshot should contain heading"
);
assert!(snapshot.contains("ref=e1"), "Snapshot should have ref e1");
assert!(snapshot.contains("ref=e2"), "Snapshot should have ref e2");
assert!(
snapshot.contains("link"),
"Snapshot should have a link element"
);
let resp = execute_command(
&json!({ "id": "4", "action": "click", "selector": "e2" }),
&mut state,
)
.await;
assert_success(&resp);
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
let resp = execute_command(&json!({ "id": "5", "action": "url" }), &mut state).await;
assert_success(&resp);
let url = get_data(&resp)["url"].as_str().unwrap();
assert!(
url.contains("iana.org"),
"Should have navigated to iana.org, got: {}",
url
);
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_screenshot() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "https://example.com" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(&json!({ "id": "3", "action": "screenshot" }), &mut state).await;
assert_success(&resp);
let path = get_data(&resp)["path"].as_str().unwrap();
assert!(path.ends_with(".png"), "Screenshot path should be .png");
let metadata = std::fs::metadata(path).expect("Screenshot file should exist");
assert!(
metadata.len() > 1000,
"Screenshot should be non-trivial size"
);
let tmp_path = std::env::temp_dir()
.join("agent-browser-e2e-test-screenshot.png")
.to_string_lossy()
.to_string();
let resp = execute_command(
&json!({ "id": "4", "action": "screenshot", "path": tmp_path }),
&mut state,
)
.await;
assert_success(&resp);
assert!(std::path::Path::new(&tmp_path).exists());
let _ = std::fs::remove_file(&tmp_path);
let resp = execute_command(
&json!({
"id": "5",
"action": "setcontent",
"html": r##"
<html><body>
<button onclick="document.getElementById('result').textContent = 'clicked'">Submit</button>
<a href="#">Home</a>
<div id="result"></div>
</body></html>
"##,
}),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "6", "action": "screenshot", "annotate": true }),
&mut state,
)
.await;
assert_success(&resp);
let annotations = get_data(&resp)["annotations"]
.as_array()
.expect("Annotated screenshot should return annotations");
assert!(
!annotations.is_empty(),
"Annotated screenshot should have at least one annotation"
);
let submit_ref = annotations
.iter()
.find(|ann| ann.get("name").and_then(|v| v.as_str()) == Some("Submit"))
.and_then(|ann| ann.get("ref").and_then(|v| v.as_str()))
.expect("Expected a Submit annotation");
let resp = execute_command(
&json!({
"id": "7",
"action": "evaluate",
"script": "document.getElementById('__agent_browser_annotations__') === null"
}),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["result"], true);
let resp = execute_command(
&json!({ "id": "8", "action": "click", "selector": format!("@{}", submit_ref) }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({
"id": "9",
"action": "evaluate",
"script": "document.getElementById('result').textContent"
}),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["result"], "clicked");
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_form_interaction() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let html = concat!(
"data:text/html,<html><body>",
"<input id='name' type='text' placeholder='Name'>",
"<input id='email' type='email'>",
"<select id='color'><option value='red'>Red</option><option value='blue'>Blue</option></select>",
"<input id='agree' type='checkbox'>",
"<textarea id='bio'></textarea>",
"<button id='submit'>Submit</button>",
"</body></html>"
);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": html }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "10", "action": "fill", "selector": "#name", "value": "John Doe" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "11", "action": "evaluate", "script": "document.getElementById('name').value" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["result"], "John Doe");
let resp = execute_command(
&json!({ "id": "12", "action": "fill", "selector": "#email", "value": "john@example.com" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "13", "action": "evaluate", "script": "document.getElementById('email').value" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["result"], "john@example.com");
let resp = execute_command(
&json!({ "id": "14", "action": "select", "selector": "#color", "values": ["blue"] }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "15", "action": "evaluate", "script": "document.getElementById('color').value" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["result"], "blue");
let resp = execute_command(
&json!({ "id": "16", "action": "check", "selector": "#agree" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "17", "action": "ischecked", "selector": "#agree" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["checked"], true);
let resp = execute_command(
&json!({ "id": "18", "action": "uncheck", "selector": "#agree" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "19", "action": "ischecked", "selector": "#agree" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["checked"], false);
let resp = execute_command(&json!({ "id": "20", "action": "snapshot" }), &mut state).await;
assert_success(&resp);
let snap = get_data(&resp)["snapshot"].as_str().unwrap();
assert!(
snap.contains("John Doe"),
"Snapshot should show filled value"
);
assert!(snap.contains("textbox"), "Snapshot should show textbox");
assert!(snap.contains("button"), "Snapshot should show button");
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_navigation_history() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "data:text/html,<h1>Page 1</h1>" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "3", "action": "navigate", "url": "data:text/html,<h1>Page 2</h1>" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(&json!({ "id": "4", "action": "back" }), &mut state).await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "5", "action": "evaluate", "script": "document.querySelector('h1').textContent" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["result"], "Page 1");
let resp = execute_command(&json!({ "id": "6", "action": "forward" }), &mut state).await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "7", "action": "evaluate", "script": "document.querySelector('h1').textContent" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["result"], "Page 2");
let resp = execute_command(&json!({ "id": "8", "action": "reload" }), &mut state).await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "9", "action": "evaluate", "script": "document.querySelector('h1').textContent" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["result"], "Page 2");
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_cookies() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "https://example.com" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({
"id": "3",
"action": "cookies_set",
"name": "test_cookie",
"value": "hello123"
}),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(&json!({ "id": "4", "action": "cookies_get" }), &mut state).await;
assert_success(&resp);
let cookies = get_data(&resp)["cookies"].as_array().unwrap();
let found = cookies
.iter()
.any(|c| c["name"] == "test_cookie" && c["value"] == "hello123");
assert!(found, "Should find the set cookie");
let resp = execute_command(&json!({ "id": "5", "action": "cookies_clear" }), &mut state).await;
assert_success(&resp);
let resp = execute_command(&json!({ "id": "6", "action": "cookies_get" }), &mut state).await;
assert_success(&resp);
let cookies = get_data(&resp)["cookies"].as_array().unwrap();
let found = cookies.iter().any(|c| c["name"] == "test_cookie");
assert!(!found, "Cookie should be cleared");
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_storage() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "https://example.com" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "3", "action": "storage_set", "type": "local", "key": "mykey", "value": "myvalue" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "4", "action": "storage_get", "type": "local", "key": "mykey" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["value"], "myvalue");
let resp = execute_command(
&json!({ "id": "5", "action": "storage_get", "type": "local" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["data"]["mykey"], "myvalue");
let resp = execute_command(
&json!({ "id": "6", "action": "storage_clear", "type": "local" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "7", "action": "storage_get", "type": "local" }),
&mut state,
)
.await;
assert_success(&resp);
let data = &get_data(&resp)["data"];
assert!(
data.as_object().map(|m| m.is_empty()).unwrap_or(true),
"Storage should be empty after clear"
);
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_tabs() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "data:text/html,<h1>Tab 1</h1>" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(&json!({ "id": "3", "action": "tab_list" }), &mut state).await;
assert_success(&resp);
let tabs = get_data(&resp)["tabs"].as_array().unwrap();
assert_eq!(tabs.len(), 1);
assert_eq!(tabs[0]["active"], true);
let resp = execute_command(
&json!({ "id": "4", "action": "tab_new", "url": "data:text/html,<h1>Tab 2</h1>" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["index"], 1);
let resp = execute_command(&json!({ "id": "5", "action": "tab_list" }), &mut state).await;
assert_success(&resp);
let tabs = get_data(&resp)["tabs"].as_array().unwrap();
assert_eq!(tabs.len(), 2);
assert_eq!(tabs[1]["active"], true);
let resp = execute_command(
&json!({ "id": "6", "action": "tab_switch", "index": 0 }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "7", "action": "evaluate", "script": "document.querySelector('h1').textContent" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["result"], "Tab 1");
let resp = execute_command(
&json!({ "id": "8", "action": "tab_close", "index": 1 }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(&json!({ "id": "9", "action": "tab_list" }), &mut state).await;
assert_success(&resp);
let tabs = get_data(&resp)["tabs"].as_array().unwrap();
assert_eq!(tabs.len(), 1);
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_element_queries() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let html = concat!(
"data:text/html,<html><body>",
"<p id='visible'>Hello World</p>",
"<p id='hidden' style='display:none'>Hidden</p>",
"<input id='enabled' value='test'>",
"<input id='disabled' disabled value='nope'>",
"<a id='link' href='https://example.com' data-testid='my-link'>Click me</a>",
"</body></html>"
);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": html }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "3", "action": "isvisible", "selector": "#visible" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["visible"], true);
let resp = execute_command(
&json!({ "id": "4", "action": "isvisible", "selector": "#hidden" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["visible"], false);
let resp = execute_command(
&json!({ "id": "5", "action": "isenabled", "selector": "#enabled" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["enabled"], true);
let resp = execute_command(
&json!({ "id": "6", "action": "isenabled", "selector": "#disabled" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["enabled"], false);
let resp = execute_command(
&json!({ "id": "7", "action": "gettext", "selector": "#visible" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["text"], "Hello World");
let resp = execute_command(
&json!({ "id": "8", "action": "getattribute", "selector": "#link", "attribute": "href" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["value"], "https://example.com");
let resp = execute_command(
&json!({ "id": "9", "action": "getattribute", "selector": "#link", "attribute": "data-testid" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["value"], "my-link");
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_wait() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let html = concat!(
"data:text/html,<html><body>",
"<div id='target' style='display:none'>Appeared!</div>",
"<script>setTimeout(() => document.getElementById('target').style.display='block', 500)</script>",
"</body></html>"
);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": html }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "3", "action": "wait", "selector": "#target", "state": "visible", "timeout": 5000 }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "4", "action": "wait", "text": "Appeared!", "timeout": 5000 }),
&mut state,
)
.await;
assert_success(&resp);
let start = std::time::Instant::now();
let resp = execute_command(
&json!({ "id": "5", "action": "wait", "timeout": 200 }),
&mut state,
)
.await;
assert_success(&resp);
assert!(
start.elapsed().as_millis() >= 150,
"Timeout wait should sleep at least 150ms"
);
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_viewport_scale_factor() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "about:blank" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "3", "action": "evaluate", "script": "window.devicePixelRatio" }),
&mut state,
)
.await;
assert_success(&resp);
let default_dpr = get_data(&resp)["result"].as_f64().unwrap();
assert_eq!(default_dpr, 1.0, "Default devicePixelRatio should be 1");
let resp = execute_command(
&json!({ "id": "4", "action": "viewport", "width": 1920, "height": 1080, "deviceScaleFactor": 2.0 }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["width"], 1920);
assert_eq!(get_data(&resp)["height"], 1080);
assert_eq!(get_data(&resp)["deviceScaleFactor"], 2.0);
let resp = execute_command(
&json!({ "id": "5", "action": "evaluate", "script": "window.devicePixelRatio" }),
&mut state,
)
.await;
assert_success(&resp);
let new_dpr = get_data(&resp)["result"].as_f64().unwrap();
assert_eq!(
new_dpr, 2.0,
"devicePixelRatio should be 2 after setting scale factor"
);
let resp = execute_command(
&json!({ "id": "6", "action": "evaluate", "script": "window.innerWidth" }),
&mut state,
)
.await;
assert_success(&resp);
let css_width = get_data(&resp)["result"].as_i64().unwrap();
assert_eq!(css_width, 1920, "CSS width should remain 1920 at 2x scale");
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_viewport_emulation() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "data:text/html,<h1>Viewport</h1>" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "3", "action": "evaluate", "script": "window.innerWidth" }),
&mut state,
)
.await;
assert_success(&resp);
let initial_width = get_data(&resp)["result"].as_i64().unwrap();
let resp = execute_command(
&json!({ "id": "4", "action": "viewport", "width": 375, "height": 812, "deviceScaleFactor": 3.0, "mobile": true }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["width"], 375);
assert_eq!(get_data(&resp)["height"], 812);
assert_eq!(get_data(&resp)["mobile"], true);
let resp = execute_command(&json!({ "id": "5", "action": "reload" }), &mut state).await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "6", "action": "evaluate", "script": "window.innerWidth" }),
&mut state,
)
.await;
assert_success(&resp);
let new_width = get_data(&resp)["result"].as_i64().unwrap();
assert!(
new_width != initial_width || new_width == 375,
"Viewport should change from {} after setDeviceMetricsOverride (got {})",
initial_width,
new_width
);
let resp = execute_command(
&json!({ "id": "5", "action": "user_agent", "userAgent": "TestBot/1.0" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "6", "action": "evaluate", "script": "navigator.userAgent" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["result"], "TestBot/1.0");
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_hover_scroll_press() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let html = concat!(
"data:text/html,<html><body style='height:3000px'>",
"<button id='btn' onmouseover=\"this.textContent='hovered'\">Hover me</button>",
"<input id='input' onkeydown=\"this.dataset.key=event.key\">",
"</body></html>"
);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": html }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "3", "action": "hover", "selector": "#btn" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "4", "action": "scroll", "y": 500 }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "5", "action": "evaluate", "script": "window.scrollY" }),
&mut state,
)
.await;
assert_success(&resp);
let scroll_y = get_data(&resp)["result"].as_f64().unwrap();
assert!(scroll_y > 0.0, "Should have scrolled down");
let resp = execute_command(
&json!({ "id": "6", "action": "press", "key": "Enter" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["pressed"], "Enter");
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_state_management() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "https://example.com" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "3", "action": "storage_set", "type": "local", "key": "persist_key", "value": "persist_val" }),
&mut state,
)
.await;
assert_success(&resp);
let tmp_state = std::env::temp_dir()
.join("agent-browser-e2e-state.json")
.to_string_lossy()
.to_string();
let resp = execute_command(
&json!({ "id": "4", "action": "state_save", "path": &tmp_state }),
&mut state,
)
.await;
assert_success(&resp);
assert!(std::path::Path::new(&tmp_state).exists());
let resp = execute_command(
&json!({ "id": "5", "action": "state_show", "path": &tmp_state }),
&mut state,
)
.await;
assert_success(&resp);
let state_data = get_data(&resp);
assert!(state_data.get("state").is_some());
let resp = execute_command(&json!({ "id": "6", "action": "state_list" }), &mut state).await;
assert_success(&resp);
assert!(get_data(&resp)["files"].is_array());
let _ = std::fs::remove_file(&tmp_state);
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_domain_filter() {
let mut state = DaemonState::new();
state.domain_filter = Some(super::network::DomainFilter::new("example.com"));
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "https://example.com" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "3", "action": "navigate", "url": "https://blocked.com" }),
&mut state,
)
.await;
assert_eq!(resp["success"], false);
let error = resp["error"].as_str().unwrap();
assert!(
error.contains("blocked") || error.contains("not allowed"),
"Should reject blocked domain, got: {}",
error
);
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_diff_snapshot() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "data:text/html,<h1>Hello</h1><p>World</p>" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(&json!({ "id": "3", "action": "snapshot" }), &mut state).await;
assert_success(&resp);
let baseline = get_data(&resp)["snapshot"].as_str().unwrap().to_string();
let resp = execute_command(
&json!({ "id": "4", "action": "evaluate", "script": "document.querySelector('h1').textContent = 'Changed'" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "5", "action": "diff_snapshot", "baseline": baseline }),
&mut state,
)
.await;
assert_success(&resp);
let diff = &get_data(&resp)["diff"];
assert_eq!(diff["identical"], false, "Diff should detect the h1 change");
assert!(
diff["additions"].as_i64().unwrap() > 0 || diff["deletions"].as_i64().unwrap() > 0,
"Should have additions or deletions"
);
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_phase8_commands() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let html = concat!(
"data:text/html,<html><body>",
"<input id='a' value='original'>",
"<input id='b' value='other'>",
"<p class='item'>One</p>",
"<p class='item'>Two</p>",
"<p class='item'>Three</p>",
"<div id='box' style='width:200px;height:100px;background:red'>Box</div>",
"</body></html>"
);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": html }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "10", "action": "focus", "selector": "#a" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "11", "action": "clear", "selector": "#a" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "12", "action": "evaluate", "script": "document.getElementById('a').value" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["result"], "");
let resp = execute_command(
&json!({ "id": "13", "action": "setvalue", "selector": "#b", "value": "new-value" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "14", "action": "inputvalue", "selector": "#b" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["value"], "new-value");
let resp = execute_command(
&json!({ "id": "15", "action": "count", "selector": ".item" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["count"], 3);
let resp = execute_command(
&json!({ "id": "16", "action": "boundingbox", "selector": "#box" }),
&mut state,
)
.await;
assert_success(&resp);
let bbox = get_data(&resp);
assert_eq!(bbox["width"], 200.0);
assert_eq!(bbox["height"], 100.0);
assert!(bbox["x"].as_f64().is_some());
assert!(bbox["y"].as_f64().is_some());
let resp = execute_command(
&json!({ "id": "17", "action": "innertext", "selector": "#box" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["text"], "Box");
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_auto_launch() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "navigate", "url": "data:text/html,<h1>Auto</h1>" }),
&mut state,
)
.await;
assert_success(&resp);
assert!(state.browser.is_some(), "Browser should be auto-launched");
let resp = execute_command(
&json!({ "id": "2", "action": "evaluate", "script": "document.querySelector('h1').textContent" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["result"], "Auto");
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_error_handling() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "data:text/html,<h1>Errors</h1>" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "10", "action": "nonexistent_action" }),
&mut state,
)
.await;
assert_eq!(resp["success"], false);
assert!(resp["error"]
.as_str()
.unwrap()
.contains("Not yet implemented"));
let resp = execute_command(
&json!({ "id": "11", "action": "fill", "selector": "#x" }),
&mut state,
)
.await;
assert_eq!(resp["success"], false);
assert!(resp["error"].as_str().unwrap().contains("value"));
let resp = execute_command(
&json!({ "id": "12", "action": "click", "selector": "#does-not-exist" }),
&mut state,
)
.await;
assert_eq!(resp["success"], false);
let resp = execute_command(
&json!({ "id": "13", "action": "evaluate", "script": "}{invalid" }),
&mut state,
)
.await;
assert_eq!(resp["success"], false);
assert!(resp["error"].as_str().unwrap().contains("error"));
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_profile_cookie_persistence() {
let profile_dir = std::env::temp_dir().join(format!(
"agent-browser-e2e-profile-{}",
uuid::Uuid::new_v4()
));
{
let mut state = DaemonState::new();
let resp = execute_command(
&json!({
"id": "1",
"action": "launch",
"headless": true,
"profile": profile_dir.to_str().unwrap()
}),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "https://example.com" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({
"id": "3",
"action": "cookies_set",
"name": "persist_test",
"value": "should_survive_restart",
"domain": ".example.com",
"path": "/",
"expires": 2000000000
}),
&mut state,
)
.await;
assert_success(&resp);
let resp =
execute_command(&json!({ "id": "4", "action": "cookies_get" }), &mut state).await;
assert_success(&resp);
let cookies = get_data(&resp)["cookies"].as_array().unwrap();
let found = cookies
.iter()
.any(|c| c["name"] == "persist_test" && c["value"] == "should_survive_restart");
assert!(found, "Cookie should exist before close");
let resp = execute_command(&json!({ "id": "5", "action": "close" }), &mut state).await;
assert_success(&resp);
}
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
{
let mut state = DaemonState::new();
let resp = execute_command(
&json!({
"id": "10",
"action": "launch",
"headless": true,
"profile": profile_dir.to_str().unwrap()
}),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "11", "action": "navigate", "url": "https://example.com" }),
&mut state,
)
.await;
assert_success(&resp);
let resp =
execute_command(&json!({ "id": "12", "action": "cookies_get" }), &mut state).await;
assert_success(&resp);
let cookies = get_data(&resp)["cookies"].as_array().unwrap();
let found = cookies
.iter()
.any(|c| c["name"] == "persist_test" && c["value"] == "should_survive_restart");
assert!(
found,
"Cookie should persist across restart with --profile. Cookies found: {:?}",
cookies
.iter()
.map(|c| c["name"].as_str().unwrap_or("?"))
.collect::<Vec<_>>()
);
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
let _ = std::fs::remove_dir_all(&profile_dir);
}
#[tokio::test]
#[ignore]
async fn e2e_get_cdp_url() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(&json!({ "id": "2", "action": "cdp_url" }), &mut state).await;
assert_success(&resp);
let cdp_url = get_data(&resp)["cdpUrl"]
.as_str()
.expect("cdpUrl should be a string");
assert!(
cdp_url.starts_with("ws://"),
"CDP URL should start with ws://, got: {}",
cdp_url
);
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_inspect() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "https://example.com" }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(&json!({ "id": "3", "action": "inspect" }), &mut state).await;
assert_success(&resp);
let data = get_data(&resp);
assert_eq!(data["opened"], true);
let url = data["url"]
.as_str()
.expect("inspect url should be a string");
assert!(
url.starts_with("http://127.0.0.1:"),
"Inspect URL should be http://127.0.0.1:<port>, got: {}",
url
);
let http_resp = reqwest::get(url).await;
match http_resp {
Ok(r) => {
let final_url = r.url().to_string();
assert!(
final_url.contains("devtools/devtools_app.html"),
"Redirect should point to DevTools frontend, got: {}",
final_url
);
}
Err(e) => {
panic!("HTTP GET to inspect URL failed: {}", e);
}
}
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}