#![cfg(feature = "integration-tests")]
#![allow(clippy::panic, clippy::unwrap_used)]
use std::time::Duration;
use serial_test::serial;
use zendriver::Browser;
use zendriver::stealth::StealthProfile;
#[tokio::test]
#[serial]
async fn ua_override_persists_across_new_tabs() {
const UA: &str = "ZendriverPortUA/0.1";
let browser = Browser::builder()
.headless(true)
.stealth(StealthProfile::native().user_agent(UA))
.launch()
.await
.expect("launch");
let main = browser.main_tab();
let main_ua: String = main
.evaluate("navigator.userAgent")
.await
.expect("read UA on main tab");
assert_eq!(
main_ua, UA,
"main tab must report the overridden UA, got {main_ua:?}"
);
for i in 0..3 {
let tab = browser
.new_tab()
.await
.unwrap_or_else(|e| panic!("open new tab #{i}: {e}"));
let ua: String = tab
.evaluate("navigator.userAgent")
.await
.unwrap_or_else(|e| panic!("read UA on new tab #{i}: {e}"));
assert_eq!(
ua, UA,
"new tab #{i} must inherit overridden UA, got {ua:?}"
);
}
browser.close().await.expect("close");
}
#[cfg(unix)]
fn ps_chrome_pids() -> std::collections::HashSet<u32> {
use std::process::Command;
let out = Command::new("ps")
.args(["axo", "pid,comm"])
.output()
.expect("ps");
let s = String::from_utf8_lossy(&out.stdout);
s.lines()
.filter(|l| {
let lower = l.to_lowercase();
lower.contains("chrome") || lower.contains("chromium")
})
.filter_map(|l| l.split_whitespace().next()?.parse::<u32>().ok())
.collect()
}
#[cfg(unix)]
#[tokio::test]
#[serial]
async fn repeated_launch_close_leaves_no_orphan_chrome_processes() {
let before = ps_chrome_pids();
for i in 0..25 {
let browser = Browser::builder()
.headless(true)
.launch()
.await
.unwrap_or_else(|e| panic!("launch iter {i}: {e}"));
let _ = browser.main_tab().goto("about:blank").await;
browser
.close()
.await
.unwrap_or_else(|e| panic!("close iter {i}: {e}"));
}
tokio::time::sleep(Duration::from_millis(500)).await;
let after = ps_chrome_pids();
let leaked: Vec<u32> = after.difference(&before).copied().collect();
assert!(
leaked.is_empty(),
"{} Chrome PIDs leaked after 25 launch/close cycles: {:?}",
leaked.len(),
leaked,
);
}
#[tokio::test]
#[serial]
async fn browser_close_during_inflight_cdp_calls_does_not_hang() {
use std::sync::Arc;
use tokio::time::timeout;
const N: usize = 10;
let browser = Browser::builder()
.headless(true)
.launch()
.await
.expect("launch");
let main = browser.main_tab();
main.goto("about:blank").await.expect("goto");
let tab = Arc::new(main);
let mut handles = Vec::with_capacity(N);
for i in 0..N {
let tab = Arc::clone(&tab);
handles.push(tokio::spawn(async move {
let result: Result<serde_json::Value, _> = tab
.evaluate("new Promise(r => setTimeout(() => r(42), 1000))")
.await;
(i, result)
}));
}
tokio::time::sleep(Duration::from_millis(50)).await;
browser.close().await.expect("close");
let mut hung = Vec::new();
for h in handles {
match timeout(Duration::from_secs(5), h).await {
Ok(Ok((_i, _call_result))) => {
}
Ok(Err(join_err)) => {
panic!("task panicked while awaiting in-flight call: {join_err}");
}
Err(_elapsed) => {
hung.push(());
}
}
}
assert!(
hung.is_empty(),
"{} of {} in-flight CDP calls hung past close (5s timeout)",
hung.len(),
N,
);
}