#![cfg(feature = "integration")]
mod common;
use std::time::Duration;
use viewpoint_core::Browser;
use common::init_tracing;
#[cfg(unix)]
fn count_zombie_chromium_processes() -> usize {
use std::process::Command;
let output = Command::new("ps")
.args(["-eo", "pid,ppid,stat,comm"])
.output();
match output {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let our_pid = std::process::id();
stdout
.lines()
.filter(|line| {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 4 {
let ppid = parts[1].parse::<u32>().unwrap_or(0);
let stat = parts[2];
let comm = parts[3];
ppid == our_pid
&& stat.starts_with('Z')
&& (comm.contains("chrom") || comm.contains("Chrom"))
} else {
false
}
})
.count()
}
Err(_) => 0,
}
}
#[tokio::test]
#[cfg(unix)]
async fn test_no_zombie_after_close() {
init_tracing();
let zombies_before = count_zombie_chromium_processes();
let browser = Browser::launch()
.headless(true)
.timeout(Duration::from_secs(30))
.launch()
.await
.expect("Failed to launch browser");
browser.close().await.expect("Failed to close browser");
tokio::time::sleep(Duration::from_millis(100)).await;
let zombies_after = count_zombie_chromium_processes();
assert!(
zombies_after <= zombies_before,
"New zombie processes detected after close: before={}, after={}",
zombies_before,
zombies_after
);
}
#[tokio::test]
#[cfg(unix)]
async fn test_no_zombie_after_drop() {
init_tracing();
let zombies_before = count_zombie_chromium_processes();
{
let browser = Browser::launch()
.headless(true)
.timeout(Duration::from_secs(30))
.launch()
.await
.expect("Failed to launch browser");
assert!(browser.is_owned());
}
tokio::time::sleep(Duration::from_millis(100)).await;
let zombies_after = count_zombie_chromium_processes();
assert!(
zombies_after <= zombies_before,
"New zombie processes detected after drop: before={}, after={}",
zombies_before,
zombies_after
);
}
#[tokio::test]
#[cfg(unix)]
async fn test_no_zombie_when_process_dies_before_close() {
use nix::sys::signal::{kill, Signal};
use nix::unistd::Pid;
init_tracing();
let zombies_before = count_zombie_chromium_processes();
let browser = Browser::launch()
.headless(true)
.timeout(Duration::from_secs(30))
.launch()
.await
.expect("Failed to launch browser");
let our_pid = std::process::id();
let output = std::process::Command::new("ps")
.args(["-eo", "pid,ppid,comm"])
.output()
.expect("Failed to run ps");
let stdout = String::from_utf8_lossy(&output.stdout);
let chromium_pid: Option<i32> = stdout.lines().find_map(|line| {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
let pid = parts[0].parse::<i32>().ok()?;
let ppid = parts[1].parse::<u32>().ok()?;
let comm = parts[2];
if ppid == our_pid && (comm.contains("chrom") || comm.contains("Chrom")) {
return Some(pid);
}
}
None
});
if let Some(pid) = chromium_pid {
let _ = kill(Pid::from_raw(pid), Signal::SIGKILL);
tokio::time::sleep(Duration::from_millis(100)).await;
}
browser.close().await.expect("Failed to close browser");
tokio::time::sleep(Duration::from_millis(100)).await;
let zombies_after = count_zombie_chromium_processes();
assert!(
zombies_after <= zombies_before,
"New zombie processes detected after close of killed browser: before={}, after={}",
zombies_before,
zombies_after
);
}
#[tokio::test]
async fn test_browser_launch_and_close() {
init_tracing();
let browser = Browser::launch()
.headless(true)
.timeout(Duration::from_secs(30))
.launch()
.await
.expect("Failed to launch browser");
assert!(browser.is_owned());
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_browser_context_creation() {
init_tracing();
let browser = Browser::launch()
.headless(true)
.launch()
.await
.expect("Failed to launch browser");
let context = browser
.new_context()
.await
.expect("Failed to create context");
assert!(!context.id().is_empty());
assert!(!context.is_closed());
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_page_creation() {
init_tracing();
let browser = Browser::launch()
.headless(true)
.launch()
.await
.expect("Failed to launch browser");
let context = browser
.new_context()
.await
.expect("Failed to create context");
let page = context.new_page().await.expect("Failed to create page");
assert!(!page.target_id().is_empty());
assert!(!page.session_id().is_empty());
assert!(!page.frame_id().is_empty());
assert!(!page.is_closed());
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_page_close() {
init_tracing();
let browser = Browser::launch()
.headless(true)
.launch()
.await
.expect("Failed to launch browser");
let context = browser
.new_context()
.await
.expect("Failed to create context");
let mut page = context.new_page().await.expect("Failed to create page");
assert!(!page.is_closed());
page.close().await.expect("Failed to close page");
assert!(page.is_closed());
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_context_close() {
init_tracing();
let browser = Browser::launch()
.headless(true)
.launch()
.await
.expect("Failed to launch browser");
let mut context = browser
.new_context()
.await
.expect("Failed to create context");
let _page1 = context.new_page().await.expect("Failed to create page 1");
let _page2 = context.new_page().await.expect("Failed to create page 2");
assert!(!context.is_closed());
context.close().await.expect("Failed to close context");
assert!(context.is_closed());
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_browser_contexts_launched() {
init_tracing();
let browser = Browser::launch()
.headless(true)
.launch()
.await
.expect("Failed to launch browser");
let _context1 = browser
.new_context()
.await
.expect("Failed to create context 1");
let _context2 = browser
.new_context()
.await
.expect("Failed to create context 2");
let contexts = browser.contexts().await.expect("Failed to get contexts");
assert!(
contexts.len() >= 3,
"Expected at least 3 contexts, got {}",
contexts.len()
);
let has_default = contexts.iter().any(|c| c.is_default());
assert!(has_default, "Should have a default context");
let _owned_count = contexts.iter().filter(|c| c.is_owned()).count();
let non_owned_count = contexts.iter().filter(|c| !c.is_owned()).count();
assert!(
non_owned_count >= 1,
"Should have at least 1 non-owned context (default)"
);
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_context_ownership_on_close() {
init_tracing();
let browser = Browser::launch()
.headless(true)
.launch()
.await
.expect("Failed to launch browser");
let mut owned_context = browser
.new_context()
.await
.expect("Failed to create context");
assert!(
owned_context.is_owned(),
"Context created with new_context() should be owned"
);
let contexts = browser.contexts().await.expect("Failed to get contexts");
let default_context = contexts.into_iter().find(|c| c.is_default());
assert!(default_context.is_some(), "Should find default context");
let mut default_ctx = default_context.unwrap();
assert!(
!default_ctx.is_owned(),
"Default context from contexts() should not be owned"
);
default_ctx
.close()
.await
.expect("Closing non-owned context should succeed");
owned_context
.close()
.await
.expect("Closing owned context should succeed");
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_default_context_pages() {
init_tracing();
let browser = Browser::launch()
.headless(true)
.launch()
.await
.expect("Failed to launch browser");
let contexts = browser.contexts().await.expect("Failed to get contexts");
let default_context = contexts.into_iter().find(|c| c.is_default());
assert!(default_context.is_some(), "Should find default context");
let default_ctx = default_context.unwrap();
let pages = default_ctx.pages().await.expect("Failed to get pages");
tracing::info!("Default context has {} pages", pages.len());
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_connect_over_cdp_invalid_url() {
init_tracing();
let result = Browser::connect_over_cdp("ftp://localhost:9222")
.timeout(Duration::from_secs(5))
.connect()
.await;
assert!(result.is_err(), "Should fail with invalid URL scheme");
let err = result.unwrap_err();
tracing::info!("Got expected error: {}", err);
}
#[tokio::test]
async fn test_connect_over_cdp_unreachable() {
init_tracing();
let result = Browser::connect_over_cdp("http://127.0.0.1:59999")
.timeout(Duration::from_secs(2))
.connect()
.await;
assert!(result.is_err(), "Should fail with unreachable endpoint");
let err = result.unwrap_err();
tracing::info!("Got expected error: {}", err);
}
#[tokio::test]
async fn test_connect_over_cdp_timeout() {
init_tracing();
let result = Browser::connect_over_cdp("http://10.255.255.1:9222")
.timeout(Duration::from_millis(500))
.connect()
.await;
assert!(
result.is_err(),
"Should fail with timeout or connection error"
);
let err = result.unwrap_err();
tracing::info!("Got expected error: {}", err);
}
#[tokio::test]
async fn test_user_data_dir_persistence() {
init_tracing();
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let user_data_path = temp_dir.path().to_path_buf();
{
let browser = Browser::launch()
.headless(true)
.user_data_dir(&user_data_path)
.timeout(Duration::from_secs(30))
.launch()
.await
.expect("Failed to launch first browser");
let context = browser
.new_context()
.await
.expect("Failed to create context");
let _page = context.new_page().await.expect("Failed to create page");
browser
.close()
.await
.expect("Failed to close first browser");
}
let entries_after_first: Vec<_> = std::fs::read_dir(&user_data_path)
.expect("Failed to read user data dir")
.collect();
tracing::info!(
"User data directory has {} entries after first session",
entries_after_first.len()
);
assert!(
!entries_after_first.is_empty(),
"Browser should create profile data in user data directory"
);
{
let browser = Browser::launch()
.headless(true)
.user_data_dir(&user_data_path)
.timeout(Duration::from_secs(30))
.launch()
.await
.expect("Failed to launch second browser with same profile");
assert!(browser.is_owned());
browser
.close()
.await
.expect("Failed to close second browser");
}
let entries_after_second: Vec<_> = std::fs::read_dir(&user_data_path)
.expect("Failed to read user data dir")
.collect();
tracing::info!(
"User data directory has {} entries after second session",
entries_after_second.len()
);
assert!(
!entries_after_second.is_empty(),
"Profile data should persist after second session"
);
}
#[tokio::test]
async fn test_user_data_dir_launch() {
init_tracing();
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let browser = Browser::launch()
.headless(true)
.user_data_dir(temp_dir.path())
.timeout(Duration::from_secs(30))
.launch()
.await
.expect("Failed to launch browser with user data dir");
assert!(browser.is_owned());
let entries: Vec<_> = std::fs::read_dir(temp_dir.path())
.expect("Failed to read user data dir")
.collect();
tracing::info!(
"User data directory contains {} entries after browser launch",
entries.len()
);
assert!(
!entries.is_empty(),
"Browser should create files in user data directory"
);
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_temp_user_data_dir_default() {
init_tracing();
let browser = Browser::launch()
.headless(true)
.timeout(Duration::from_secs(30))
.launch()
.await
.expect("Failed to launch browser with default temp profile");
assert!(browser.is_owned());
let context = browser
.new_context()
.await
.expect("Failed to create context");
let _page = context.new_page().await.expect("Failed to create page");
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_concurrent_browser_launches_no_conflict() {
init_tracing();
let (browser1_result, browser2_result) = tokio::join!(
Browser::launch()
.headless(true)
.timeout(Duration::from_secs(30))
.launch(),
Browser::launch()
.headless(true)
.timeout(Duration::from_secs(30))
.launch()
);
let browser1 = browser1_result.expect("Failed to launch first browser");
let browser2 = browser2_result.expect("Failed to launch second browser");
assert!(browser1.is_owned());
assert!(browser2.is_owned());
let _context1 = browser1
.new_context()
.await
.expect("Failed to create context in browser 1");
let _context2 = browser2
.new_context()
.await
.expect("Failed to create context in browser 2");
browser1.close().await.expect("Failed to close browser 1");
browser2.close().await.expect("Failed to close browser 2");
}
#[tokio::test]
async fn test_temp_directory_cleanup_on_close() {
init_tracing();
let browser = Browser::launch()
.headless(true)
.timeout(Duration::from_secs(30))
.launch()
.await
.expect("Failed to launch browser");
let context = browser
.new_context()
.await
.expect("Failed to create context");
let _page = context.new_page().await.expect("Failed to create page");
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_temp_directory_cleanup_on_drop() {
init_tracing();
{
let browser = Browser::launch()
.headless(true)
.timeout(Duration::from_secs(30))
.launch()
.await
.expect("Failed to launch browser");
assert!(browser.is_owned());
}
}
#[tokio::test]
async fn test_template_profile_copy() {
init_tracing();
let template_dir = tempfile::tempdir().expect("Failed to create template dir");
let test_file_path = template_dir.path().join("test_file.txt");
std::fs::write(&test_file_path, "template content").expect("Failed to write test file");
let sub_dir = template_dir.path().join("subdir");
std::fs::create_dir(&sub_dir).expect("Failed to create subdir");
std::fs::write(sub_dir.join("nested.txt"), "nested content").expect("Failed to write nested file");
let browser = Browser::launch()
.headless(true)
.user_data_dir_template_from(template_dir.path())
.timeout(Duration::from_secs(30))
.launch()
.await
.expect("Failed to launch browser with template profile");
assert!(browser.is_owned());
assert!(test_file_path.exists(), "Template test file should still exist");
assert!(sub_dir.join("nested.txt").exists(), "Template nested file should still exist");
let context = browser
.new_context()
.await
.expect("Failed to create context");
let _page = context.new_page().await.expect("Failed to create page");
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_template_profile_nonexistent_path() {
init_tracing();
let result = Browser::launch()
.headless(true)
.user_data_dir_template_from("/nonexistent/path/to/template")
.timeout(Duration::from_secs(5))
.launch()
.await;
assert!(result.is_err(), "Should fail with non-existent template path");
let err = result.unwrap_err();
let err_msg = format!("{err}");
assert!(
err_msg.contains("does not exist"),
"Error should mention path does not exist: {err_msg}"
);
}