mod common;
use common::{execute, skip_if_no_chrome};
use earl_protocol_browser::PreparedBrowserCommand;
use earl_protocol_browser::schema::BrowserStep;
use std::collections::HashMap;
#[tokio::test]
async fn extract_static_text_via_evaluate() {
if skip_if_no_chrome() {
return;
}
let _guard = common::chrome_lock().await;
let mut routes = HashMap::new();
routes.insert(
"GET /".to_string(),
common::server::Response::html("<h1>Product: Acme Widget</h1>"),
);
let server = common::server::spawn(routes).await;
let data = PreparedBrowserCommand {
session_id: None,
headless: true,
timeout_ms: 30_000,
on_failure_screenshot: false,
steps: vec![
BrowserStep::Navigate {
url: server.url("/"),
expected_status: None,
timeout_ms: None,
optional: false,
},
BrowserStep::Evaluate {
function: "() => document.querySelector('h1').textContent".to_string(),
r#ref: None,
timeout_ms: None,
optional: false,
},
],
};
let result = execute(data).await.expect("execute should succeed");
assert_eq!(
result["value"],
serde_json::Value::String("Product: Acme Widget".to_string()),
"evaluate should return the h1 text content; got: {result}"
);
}
#[tokio::test]
async fn extract_dynamic_text_with_wait_for() {
if skip_if_no_chrome() {
return;
}
let _guard = common::chrome_lock().await;
let body = r#"<html><body>
<script>
setTimeout(function() {
var p = document.createElement('p');
p.id = 'loaded';
p.textContent = 'Data loaded';
document.body.appendChild(p);
}, 300);
</script>
</body></html>"#;
let mut routes = HashMap::new();
routes.insert("GET /".to_string(), common::server::Response::html(body));
let server = common::server::spawn(routes).await;
let data = PreparedBrowserCommand {
session_id: None,
headless: true,
timeout_ms: 30_000,
on_failure_screenshot: false,
steps: vec![
BrowserStep::Navigate {
url: server.url("/"),
expected_status: None,
timeout_ms: None,
optional: false,
},
BrowserStep::WaitFor {
time: None,
text: Some("Data loaded".to_string()),
text_gone: None,
timeout_ms: Some(5_000),
optional: false,
},
BrowserStep::Evaluate {
function: "() => document.getElementById('loaded').textContent".to_string(),
r#ref: None,
timeout_ms: None,
optional: false,
},
],
};
let result = execute(data).await.expect("execute should succeed");
assert_eq!(
result["value"],
serde_json::Value::String("Data loaded".to_string()),
"evaluate should return the dynamically-injected text; got: {result}"
);
}
#[tokio::test]
async fn wait_for_times_out_when_content_absent() {
if skip_if_no_chrome() {
return;
}
let _guard = common::chrome_lock().await;
let mut routes = HashMap::new();
routes.insert(
"GET /".to_string(),
common::server::Response::html("<html><body></body></html>"),
);
let server = common::server::spawn(routes).await;
let data = PreparedBrowserCommand {
session_id: None,
headless: true,
timeout_ms: 30_000,
on_failure_screenshot: false,
steps: vec![
BrowserStep::Navigate {
url: server.url("/"),
expected_status: None,
timeout_ms: None,
optional: false,
},
BrowserStep::WaitFor {
time: None,
text: Some("content that will never appear".to_string()),
text_gone: None,
timeout_ms: Some(500),
optional: false,
},
],
};
let err = execute(data)
.await
.expect_err("execute should fail with a timeout error");
let msg = err.to_string();
assert!(
msg.to_lowercase().contains("wait_for")
|| msg.to_lowercase().contains("timed out")
|| msg.to_lowercase().contains("timeout"),
"error should mention a timeout or wait_for; got: {msg}"
);
}
#[tokio::test]
async fn multi_navigate_snapshot_reflects_last_page() {
if skip_if_no_chrome() {
return;
}
let _guard = common::chrome_lock().await;
let mut routes = HashMap::new();
routes.insert(
"GET /page1".to_string(),
common::server::Response::html(
"<html><head><title>Page One</title></head><body><p>Page One content</p></body></html>",
),
);
routes.insert(
"GET /page2".to_string(),
common::server::Response::html(
"<html><head><title>Page Two</title></head><body><p>Page Two content</p></body></html>",
),
);
let server = common::server::spawn(routes).await;
let data = PreparedBrowserCommand {
session_id: None,
headless: true,
timeout_ms: 30_000,
on_failure_screenshot: false,
steps: vec![
BrowserStep::Navigate {
url: server.url("/page1"),
expected_status: None,
timeout_ms: None,
optional: false,
},
BrowserStep::Navigate {
url: server.url("/page2"),
expected_status: None,
timeout_ms: None,
optional: false,
},
BrowserStep::Snapshot {
timeout_ms: None,
optional: false,
},
],
};
let result = execute(data).await.expect("execute should succeed");
let text = result["text"]
.as_str()
.expect("snapshot result should have a 'text' string field");
assert!(
text.contains("Page Two"),
"snapshot text should contain 'Page Two' (the second page); got: {text}"
);
}
#[tokio::test]
async fn wait_for_text_gone_waits_until_text_disappears() {
if skip_if_no_chrome() {
return;
}
let _guard = common::chrome_lock().await;
let body = r#"<html><body>
<div id="status">Loading...</div>
<script>
setTimeout(function() {
document.getElementById('status').textContent = 'Done';
}, 200);
</script>
</body></html>"#;
let mut routes = HashMap::new();
routes.insert("GET /".to_string(), common::server::Response::html(body));
let server = common::server::spawn(routes).await;
let data = PreparedBrowserCommand {
session_id: None,
headless: true,
timeout_ms: 30_000,
on_failure_screenshot: false,
steps: vec![
BrowserStep::Navigate {
url: server.url("/"),
expected_status: None,
timeout_ms: None,
optional: false,
},
BrowserStep::WaitFor {
time: None,
text: Some("Done".to_string()),
text_gone: Some("Loading...".to_string()),
timeout_ms: Some(5_000),
optional: false,
},
BrowserStep::Evaluate {
function: "() => document.body.innerText".to_string(),
r#ref: None,
timeout_ms: None,
optional: false,
},
],
};
let result = execute(data).await.expect("execute should succeed");
let text = result["value"]
.as_str()
.expect("evaluate should return a string value");
assert!(
text.contains("Done"),
"page text should contain 'Done' after transition; got: {text}"
);
assert!(
!text.contains("Loading"),
"page text should no longer contain 'Loading' after transition; got: {text}"
);
}
#[tokio::test]
async fn navigate_expected_status_succeeds_on_match() {
if skip_if_no_chrome() {
return;
}
let _guard = common::chrome_lock().await;
let mut routes = HashMap::new();
routes.insert(
"GET /missing".to_string(),
common::server::Response {
status: 404,
content_type: "text/plain",
body: "gone".to_string(),
extra_headers: vec![],
},
);
let server = common::server::spawn(routes).await;
let data = PreparedBrowserCommand {
session_id: None,
headless: true,
timeout_ms: 30_000,
on_failure_screenshot: false,
steps: vec![BrowserStep::Navigate {
url: server.url("/missing"),
expected_status: Some(404),
timeout_ms: None,
optional: false,
}],
};
execute(data)
.await
.expect("execute should succeed when expected_status matches the actual status");
}
#[tokio::test]
async fn navigate_expected_status_mismatch_fails() {
if skip_if_no_chrome() {
return;
}
let _guard = common::chrome_lock().await;
let mut routes = HashMap::new();
routes.insert(
"GET /".to_string(),
common::server::Response::html("<html><body>OK</body></html>"),
);
let server = common::server::spawn(routes).await;
let data = PreparedBrowserCommand {
session_id: None,
headless: true,
timeout_ms: 30_000,
on_failure_screenshot: false,
steps: vec![BrowserStep::Navigate {
url: server.url("/"),
expected_status: Some(404),
timeout_ms: None,
optional: false,
}],
};
let err = execute(data)
.await
.expect_err("should fail when status doesn't match expected");
let msg = err.to_string();
assert!(
msg.contains("404") || msg.contains("200") || msg.to_lowercase().contains("status"),
"error message should mention status code; got: {msg}"
);
}