mod support;
use std::net::TcpListener;
use std::path::PathBuf;
use std::time::Duration;
use cdp_browser_lite::browser::Browser;
use cdp_browser_lite::config::{BrowserConfig, LaunchMode, ProfileMode};
use cdp_browser_lite::error::BrowserError;
use support::fake_chrome::{FakeMode, fake_chrome_path};
use support::mock_devtools::{MockBehavior, MockChrome};
fn pick_free_port() -> u16 {
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let p = l.local_addr().unwrap().port();
drop(l);
p
}
fn fresh_user_data_dir(label: &str) -> PathBuf {
let dir = tempfile::Builder::new()
.prefix(&format!("cdp-browser-lifecycle-{label}-"))
.tempdir()
.unwrap()
.keep();
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn fake_chrome_config(port: u16, user_data_dir: &std::path::Path) -> BrowserConfig {
BrowserConfig::builder()
.mode(LaunchMode::LaunchNew)
.chrome_path(fake_chrome_path())
.port(port)
.headless(true)
.startup_timeout(Duration::from_secs(5))
.profile(ProfileMode::Persistent(user_data_dir.to_path_buf()))
.build()
}
fn set_fake_chrome_env(mode: FakeMode, args_log: &std::path::Path) {
unsafe {
std::env::set_var("FAKE_CHROME_MODE", mode.env_value());
std::env::set_var("FAKE_CHROME_ARGS_LOG", args_log.to_string_lossy().as_ref());
}
}
fn auto_config_fake(port: u16, user_data_dir: &std::path::Path) -> BrowserConfig {
BrowserConfig::builder()
.mode(LaunchMode::Auto)
.chrome_path(fake_chrome_path())
.port(port)
.headless(true)
.startup_timeout(Duration::from_secs(5))
.profile(ProfileMode::Persistent(user_data_dir.to_path_buf()))
.build()
}
#[tokio::test]
async fn auto_attach_to_user_chrome_regression_issue_4() {
let mock = MockChrome::start(MockBehavior::KeepAlive).await;
let cfg = BrowserConfig::builder()
.mode(LaunchMode::Auto)
.port(mock.port)
.startup_timeout(Duration::from_secs(1))
.build();
let browser = Browser::ensure(cfg)
.await
.expect("Auto must attach to user Chrome");
assert!(
!browser.is_managed(),
"attached browser must not be managed"
);
let (host, port) = browser.debug_address();
assert_eq!(host, "127.0.0.1");
assert_eq!(port, mock.port);
}
#[tokio::test]
async fn auto_launch_when_port_is_closed() {
let port = pick_free_port();
let user_data_dir = fresh_user_data_dir("auto-launch");
let args_log = user_data_dir.join("args.log");
set_fake_chrome_env(FakeMode::Serve, &args_log);
let cfg = auto_config_fake(port, &user_data_dir);
let browser = Browser::ensure(cfg)
.await
.expect("Auto must launch when port is closed");
assert!(browser.is_managed(), "launched browser must be managed");
let (host, actual_port) = browser.debug_address();
assert_eq!(host, "127.0.0.1");
assert_eq!(actual_port, port);
}
#[tokio::test]
async fn auto_with_port_occupied_by_non_chrome() {
let occupied_port = pick_free_port();
let _squat = TcpListener::bind(("127.0.0.1", occupied_port)).unwrap();
let user_data_dir = fresh_user_data_dir("auto-non-chrome");
let args_log = user_data_dir.join("args.log");
set_fake_chrome_env(FakeMode::Serve, &args_log);
let cfg = auto_config_fake(occupied_port, &user_data_dir);
let browser = Browser::ensure(cfg)
.await
.expect("Auto must find free port when occupied by non-Chrome");
assert!(browser.is_managed());
let (_, actual_port) = browser.debug_address();
assert_ne!(
actual_port, occupied_port,
"effective port must differ from occupied base"
);
}
#[tokio::test]
async fn launch_new_always_own_process() {
let mock = MockChrome::start(MockBehavior::KeepAlive).await;
let cfg = BrowserConfig::builder()
.mode(LaunchMode::LaunchNew)
.port(mock.port)
.startup_timeout(Duration::from_secs(1))
.build();
match Browser::ensure(cfg).await {
Err(BrowserError::PortConflict { port }) => {
assert_eq!(port, mock.port);
}
other => panic!("expected PortConflict for LaunchNew on occupied port, got {other:?}"),
}
}
#[tokio::test]
async fn launch_new_ephemeral_port() {
let user_data_dir = fresh_user_data_dir("launch-new-eph");
let args_log = user_data_dir.join("args.log");
set_fake_chrome_env(FakeMode::Serve, &args_log);
let cfg = BrowserConfig::builder()
.mode(LaunchMode::LaunchNew)
.chrome_path(fake_chrome_path())
.port(0)
.headless(true)
.startup_timeout(Duration::from_secs(5))
.profile(ProfileMode::Persistent(user_data_dir.clone()))
.build();
let browser = Browser::ensure(cfg)
.await
.expect("LaunchNew with ephemeral port must succeed");
assert!(browser.is_managed());
let (_, actual_port) = browser.debug_address();
assert_ne!(actual_port, 0, "ephemeral port must resolve to non-zero");
}
#[tokio::test]
async fn attach_only_remote_unavailable() {
let port = pick_free_port();
let cfg = BrowserConfig::builder()
.mode(LaunchMode::AttachOnly)
.host("127.0.0.1")
.port(port)
.build();
match Browser::ensure(cfg).await {
Err(BrowserError::RemoteUnavailable {
host,
port: actual_port,
}) => {
assert_eq!(host, "127.0.0.1");
assert_eq!(actual_port, port);
}
other => panic!("expected RemoteUnavailable, got {other:?}"),
}
}
#[tokio::test]
async fn attach_only_to_running_chrome() {
let mock = MockChrome::start(MockBehavior::KeepAlive).await;
let cfg = BrowserConfig::builder()
.mode(LaunchMode::AttachOnly)
.port(mock.port)
.build();
let browser = Browser::ensure(cfg)
.await
.expect("AttachOnly must succeed for running Chrome");
assert!(!browser.is_managed());
let (_, port) = browser.debug_address();
assert_eq!(port, mock.port);
}
#[tokio::test]
async fn stop_kills_process_and_cleans_profile() {
let port = pick_free_port();
let user_data_dir = fresh_user_data_dir("stop-clean");
let args_log = user_data_dir.join("args.log");
set_fake_chrome_env(FakeMode::Serve, &args_log);
let cfg = fake_chrome_config(port, &user_data_dir);
let browser = Browser::ensure(cfg)
.await
.expect("ensure must succeed for managed browser");
assert!(browser.is_managed());
assert!(browser.is_alive());
browser.stop().await.expect("stop must succeed");
assert!(!browser.is_alive());
}
#[tokio::test]
async fn stop_on_attached_does_nothing() {
let mock = MockChrome::start(MockBehavior::KeepAlive).await;
let cfg = BrowserConfig::builder()
.mode(LaunchMode::Auto)
.port(mock.port)
.startup_timeout(Duration::from_secs(1))
.build();
let browser = Browser::ensure(cfg).await.expect("Auto must attach");
assert!(!browser.is_managed());
browser.stop().await.expect("stop on attached must be Ok");
assert!(!browser.is_alive(), "browser must be marked stopped");
}
#[tokio::test]
async fn restart_produces_new_process() {
let port = pick_free_port();
let user_data_dir = fresh_user_data_dir("restart");
let args_log = user_data_dir.join("args.log");
set_fake_chrome_env(FakeMode::Serve, &args_log);
let cfg = fake_chrome_config(port, &user_data_dir);
let browser = Browser::ensure(cfg).await.expect("ensure must succeed");
assert!(browser.is_managed());
assert!(browser.is_alive());
let _old_port = browser.debug_address().1;
browser.restart().await.expect("restart must succeed");
assert!(browser.is_managed());
assert!(browser.is_alive());
let new_port = browser.debug_address().1;
assert_eq!(new_port, port, "restart must reuse configured port");
assert_ne!(new_port, 0);
browser.stop().await.expect("cleanup stop must succeed");
}
#[cfg(unix)]
#[test]
#[ignore = "requires Chrome installed on the machine"]
fn e2e_ensure_stop_restart_with_real_chrome_headless() {
use std::net::TcpStream;
use std::time::Instant;
let port = pick_free_port();
let user_data_dir = fresh_user_data_dir("e2e-real");
let cfg = BrowserConfig::builder()
.mode(LaunchMode::LaunchNew)
.port(port)
.headless(true)
.startup_timeout(Duration::from_secs(10))
.profile(ProfileMode::Persistent(user_data_dir.clone()))
.build();
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let browser = Browser::ensure(cfg)
.await
.expect("real Chrome must launch headless");
assert!(browser.is_managed());
let (_, actual_port) = browser.debug_address();
assert_eq!(actual_port, port);
assert!(browser.is_alive());
browser.stop().await.expect("stop must succeed");
assert!(!browser.is_alive());
assert!(user_data_dir.exists());
let browser2 = Browser::ensure(
BrowserConfig::builder()
.mode(LaunchMode::LaunchNew)
.port(port)
.headless(true)
.startup_timeout(Duration::from_secs(10))
.profile(ProfileMode::Persistent(user_data_dir.clone()))
.build(),
)
.await
.expect("restart must succeed");
assert!(browser2.is_managed());
assert!(browser2.is_alive());
let (_, p) = browser2.debug_address();
assert_eq!(p, port);
browser2.stop().await.expect("final stop must succeed");
});
let start = Instant::now();
while start.elapsed() < Duration::from_secs(5) {
if TcpStream::connect_timeout(
&format!("127.0.0.1:{port}").parse().unwrap(),
Duration::from_millis(100),
)
.is_err()
{
return;
}
std::thread::sleep(Duration::from_millis(200));
}
panic!("port {port} should be free after full stop");
}