mod common;
use common::{execute, skip_if_no_chrome};
use earl_protocol_browser::PreparedBrowserCommand;
use earl_protocol_browser::schema::BrowserStep;
use std::collections::HashMap;
fn unique_id() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let count = COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
format!("{pid}-{count}")
}
#[tokio::test]
async fn session_call_1_succeeds() {
if skip_if_no_chrome() {
return;
}
let _guard = common::chrome_lock().await;
let session_id = format!("test-persist-{}", unique_id());
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 is here</p></body></html>",
),
);
let server = common::server::spawn(routes).await;
let call1 = PreparedBrowserCommand {
session_id: Some(session_id.clone()),
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::Snapshot {
timeout_ms: None,
optional: false,
},
],
};
let result1 = execute(call1).await.expect("call 1 should succeed");
let text1 = result1["text"]
.as_str()
.expect("call 1 snapshot should have 'text'");
assert!(
text1.contains("Page One"),
"call 1 snapshot should contain 'Page One'; got: {text1}"
);
use earl_protocol_browser::session::{lock_file_path, session_file_path};
let _ = std::fs::remove_file(session_file_path(&session_id).unwrap());
let _ = std::fs::remove_file(lock_file_path(&session_id).unwrap());
}
#[tokio::test]
async fn session_call_2_reuses_same_session() {
if skip_if_no_chrome() {
return;
}
let _guard = common::chrome_lock().await;
let session_id = format!("test-persist-{}", unique_id());
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 is here</p></body></html>",
),
);
let server = common::server::spawn(routes).await;
let call1 = PreparedBrowserCommand {
session_id: Some(session_id.clone()),
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::Snapshot {
timeout_ms: None,
optional: false,
},
],
};
let result1 = execute(call1).await.expect("call 1 should succeed");
let text1 = result1["text"]
.as_str()
.expect("call 1 snapshot should have 'text'");
assert!(
text1.contains("Page One"),
"call 1 snapshot should contain 'Page One'; got: {text1}"
);
let call2 = PreparedBrowserCommand {
session_id: Some(session_id.clone()),
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::Snapshot {
timeout_ms: None,
optional: false,
},
],
};
let result2 = execute(call2).await.expect("call 2 should succeed");
let text2 = result2["text"]
.as_str()
.expect("call 2 snapshot should have 'text'");
assert!(
text2.contains("Page One"),
"call 2 snapshot should contain 'Page One'; got: {text2}"
);
use earl_protocol_browser::session::{lock_file_path, session_file_path};
let _ = std::fs::remove_file(session_file_path(&session_id).unwrap());
let _ = std::fs::remove_file(lock_file_path(&session_id).unwrap());
}
#[tokio::test]
async fn stale_session_falls_back_to_fresh_chrome() {
if skip_if_no_chrome() {
return;
}
let _guard = common::chrome_lock().await;
use chrono::Utc;
use earl_protocol_browser::session::{
SessionFile, ensure_sessions_dir, lock_file_path, session_file_path, sessions_dir,
};
let session_id = format!("test-stale-{}", unique_id());
let dir = sessions_dir().unwrap();
ensure_sessions_dir(&dir).unwrap();
let sf = SessionFile {
pid: 0,
websocket_url: "ws://127.0.0.1:1/devtools/browser/fake".to_string(),
target_id: "fake".to_string(),
started_at: Utc::now(),
last_used_at: Utc::now(),
interrupted: false,
};
sf.save_to(&session_file_path(&session_id).unwrap())
.unwrap();
let mut routes = HashMap::new();
routes.insert(
"GET /stale-check".to_string(),
common::server::Response::html(
"<html><head><title>Fresh Chrome</title></head><body><p>Fresh Chrome loaded</p></body></html>",
),
);
let server = common::server::spawn(routes).await;
let data = PreparedBrowserCommand {
session_id: Some(session_id.clone()),
headless: true,
timeout_ms: 30_000,
on_failure_screenshot: false,
steps: vec![
BrowserStep::Navigate {
url: server.url("/stale-check"),
expected_status: None,
timeout_ms: None,
optional: false,
},
BrowserStep::Evaluate {
function: "() => document.title".to_string(),
r#ref: None,
timeout_ms: None,
optional: false,
},
],
};
let result = execute(data)
.await
.expect("stale session should fall back to fresh Chrome");
let title = result["value"]
.as_str()
.expect("evaluate should return the page title as 'value'");
assert_eq!(
title, "Fresh Chrome",
"title should be 'Fresh Chrome' confirming navigation succeeded; got: {title}"
);
let _ = std::fs::remove_file(session_file_path(&session_id).unwrap());
let _ = std::fs::remove_file(lock_file_path(&session_id).unwrap());
}
#[tokio::test]
async fn concurrent_lock_returns_session_locked_error() {
use earl_protocol_browser::session::{acquire_session_lock, lock_file_path};
let session_id = format!("test-lock-{}", unique_id());
let _lock = acquire_session_lock(&session_id).await.unwrap();
let data = PreparedBrowserCommand {
session_id: Some(session_id.clone()),
headless: true,
timeout_ms: 30_000,
on_failure_screenshot: false,
steps: vec![BrowserStep::Snapshot {
timeout_ms: None,
optional: false,
}],
};
let err = execute(data)
.await
.expect_err("execute should fail with SessionLocked");
let msg = err.to_string();
assert!(
msg.to_lowercase().contains("locked") || msg.contains("SessionLocked"),
"error should mention 'locked' or 'SessionLocked'; got: {msg}"
);
drop(_lock);
let _ = std::fs::remove_file(lock_file_path(&session_id).unwrap());
}