use crate::test_server::TestServer;
use playwright_rs::protocol::{ClickOptions, GotoOptions, Playwright};
use std::process::Command;
use std::time::Duration;
#[cfg(target_os = "linux")]
fn get_process_memory_mb() -> Option<f64> {
use std::fs;
let status = fs::read_to_string("/proc/self/status").ok()?;
for line in status.lines() {
if line.starts_with("VmRSS:") {
if let Some(kb_str) = line.split_whitespace().nth(1)
&& let Ok(kb) = kb_str.parse::<f64>()
{
return Some(kb / 1024.0); }
}
}
None
}
#[cfg(target_os = "macos")]
fn get_process_memory_mb() -> Option<f64> {
use std::process::Command;
let output = Command::new("ps")
.args(["-o", "rss=", "-p", &std::process::id().to_string()])
.output()
.ok()?;
let output_str = String::from_utf8(output.stdout).ok()?;
let kb: f64 = output_str.trim().parse().ok()?;
Some(kb / 1024.0) }
#[cfg(target_os = "windows")]
fn get_process_memory_mb() -> Option<f64> {
use std::mem;
use winapi::shared::minwindef::FALSE;
use winapi::um::processthreadsapi::GetCurrentProcess;
use winapi::um::psapi::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS};
unsafe {
let mut pmc: PROCESS_MEMORY_COUNTERS = mem::zeroed();
pmc.cb = mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32;
if GetProcessMemoryInfo(
GetCurrentProcess(),
&mut pmc as *mut _,
mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32,
) != FALSE
{
let bytes = pmc.WorkingSetSize;
return Some(bytes as f64 / (1024.0 * 1024.0)); }
}
None
}
#[tokio::test]
#[ignore] async fn test_no_memory_leak_browser_cycles() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Memory Leaks: Browser Launch/Close Cycles ===\n");
let initial_memory = get_process_memory_mb().unwrap_or(0.0);
tracing::info!("Initial memory: {:.2} MB", initial_memory);
const CYCLES: usize = 20;
let mut memory_samples = Vec::new();
for i in 0..CYCLES {
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let page = browser.new_page().await.expect("Failed to create page");
let _ = page.goto("about:blank", None).await;
let _ = page.close().await;
browser.close().await.expect("Failed to close browser");
if i % 10 == 9
&& let Some(mem) = get_process_memory_mb()
{
memory_samples.push(mem);
tracing::debug!("After {} cycles: {:.2} MB", i + 1, mem);
}
}
let final_memory = get_process_memory_mb().unwrap_or(0.0);
tracing::info!("\nFinal memory: {:.2} MB", final_memory);
tracing::info!("Memory growth: {:.2} MB", final_memory - initial_memory);
if memory_samples.len() >= 2 {
let first_half_avg: f64 = memory_samples[..memory_samples.len() / 2]
.iter()
.sum::<f64>()
/ (memory_samples.len() / 2) as f64;
let second_half_avg: f64 = memory_samples[memory_samples.len() / 2..]
.iter()
.sum::<f64>()
/ (memory_samples.len() - memory_samples.len() / 2) as f64;
let memory_growth_rate = second_half_avg - first_half_avg;
tracing::info!("First half average: {:.2} MB", first_half_avg);
tracing::info!("Second half average: {:.2} MB", second_half_avg);
tracing::info!("Growth rate: {:.2} MB", memory_growth_rate);
assert!(
memory_growth_rate < 50.0,
"Memory leak detected: growth rate {:.2} MB exceeds threshold",
memory_growth_rate
);
}
tracing::info!("\n✓ No memory leak detected in browser cycles");
}
#[tokio::test]
#[ignore] async fn test_no_memory_leak_page_cycles() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Memory Leaks: Page Creation/Destruction ===\n");
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let initial_memory = get_process_memory_mb().unwrap_or(0.0);
tracing::info!("Initial memory: {:.2} MB", initial_memory);
const CYCLES: usize = 25;
let mut memory_samples = Vec::new();
for i in 0..CYCLES {
let page = browser.new_page().await.expect("Failed to create page");
let _ = page.goto("about:blank", None).await;
page.close().await.expect("Failed to close page");
if i % 5 == 4
&& let Some(mem) = get_process_memory_mb()
{
memory_samples.push(mem);
tracing::debug!("After {} page cycles: {:.2} MB", i + 1, mem);
}
}
let final_memory = get_process_memory_mb().unwrap_or(0.0);
tracing::info!("\nFinal memory: {:.2} MB", final_memory);
tracing::info!("Memory growth: {:.2} MB", final_memory - initial_memory);
if memory_samples.len() >= 2 {
let first_half_avg: f64 = memory_samples[..memory_samples.len() / 2]
.iter()
.sum::<f64>()
/ (memory_samples.len() / 2) as f64;
let second_half_avg: f64 = memory_samples[memory_samples.len() / 2..]
.iter()
.sum::<f64>()
/ (memory_samples.len() - memory_samples.len() / 2) as f64;
let memory_growth_rate = second_half_avg - first_half_avg;
tracing::info!("First half average: {:.2} MB", first_half_avg);
tracing::info!("Second half average: {:.2} MB", second_half_avg);
tracing::info!("Growth rate: {:.2} MB", memory_growth_rate);
assert!(
memory_growth_rate < 30.0,
"Memory leak detected: growth rate {:.2} MB exceeds threshold",
memory_growth_rate
);
}
browser.close().await.expect("Failed to close browser");
tracing::info!("\n✓ No memory leak detected in page cycles");
}
#[tokio::test]
#[ignore] async fn test_no_memory_leak_context_cycles() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Memory Leaks: Context Creation/Destruction ===\n");
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let initial_memory = get_process_memory_mb().unwrap_or(0.0);
tracing::info!("Initial memory: {:.2} MB", initial_memory);
const CYCLES: usize = 25;
let mut memory_samples = Vec::new();
for i in 0..CYCLES {
let context = browser
.new_context()
.await
.expect("Failed to create context");
let page = context.new_page().await.expect("Failed to create page");
let _ = page.goto("about:blank", None).await;
let _ = page.close().await;
context.close().await.expect("Failed to close context");
if i % 5 == 4
&& let Some(mem) = get_process_memory_mb()
{
memory_samples.push(mem);
tracing::debug!("After {} context cycles: {:.2} MB", i + 1, mem);
}
}
let final_memory = get_process_memory_mb().unwrap_or(0.0);
tracing::info!("\nFinal memory: {:.2} MB", final_memory);
tracing::info!("Memory growth: {:.2} MB", final_memory - initial_memory);
if memory_samples.len() >= 2 {
let first_half_avg: f64 = memory_samples[..memory_samples.len() / 2]
.iter()
.sum::<f64>()
/ (memory_samples.len() / 2) as f64;
let second_half_avg: f64 = memory_samples[memory_samples.len() / 2..]
.iter()
.sum::<f64>()
/ (memory_samples.len() - memory_samples.len() / 2) as f64;
let memory_growth_rate = second_half_avg - first_half_avg;
tracing::info!("First half average: {:.2} MB", first_half_avg);
tracing::info!("Second half average: {:.2} MB", second_half_avg);
tracing::info!("Growth rate: {:.2} MB", memory_growth_rate);
assert!(
memory_growth_rate < 30.0,
"Memory leak detected: growth rate {:.2} MB exceeds threshold",
memory_growth_rate
);
}
browser.close().await.expect("Failed to close browser");
tracing::info!("\n✓ No memory leak detected in context cycles");
}
#[tokio::test]
#[ignore] async fn test_rapid_browser_creation() {
crate::common::init_tracing();
tracing::info!("\n=== Stress Test: Rapid Browser Creation ===\n");
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
const RAPID_CYCLES: usize = 10;
for i in 0..RAPID_CYCLES {
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
browser.close().await.expect("Failed to close browser");
if i % 5 == 4 {
tracing::debug!("Completed {} rapid cycles", i + 1);
}
}
tracing::info!("\n✓ Rapid browser creation handled successfully");
}
#[cfg(unix)]
fn count_open_file_descriptors() -> Option<usize> {
#[cfg(target_os = "linux")]
{
std::fs::read_dir("/proc/self/fd")
.ok()
.map(|entries| entries.count())
}
#[cfg(target_os = "macos")]
{
let output = Command::new("lsof")
.args(["-p", &std::process::id().to_string()])
.output()
.ok()?;
let output_str = String::from_utf8(output.stdout).ok()?;
let count = output_str.lines().count().saturating_sub(1);
Some(count)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
None
}
#[cfg(not(unix))]
#[allow(dead_code)] fn count_open_file_descriptors() -> Option<usize> {
None
}
#[cfg(unix)]
fn count_child_processes() -> Option<usize> {
let my_pid = std::process::id();
let output = Command::new("ps")
.args(["-o", "ppid=", "-e"])
.output()
.ok()?;
let output_str = String::from_utf8(output.stdout).ok()?;
let count = output_str
.lines()
.filter(|line| {
if let Ok(ppid) = line.trim().parse::<u32>() {
ppid == my_pid
} else {
false
}
})
.count();
Some(count)
}
#[cfg(not(unix))]
#[allow(dead_code)] fn count_child_processes() -> Option<usize> {
None
}
#[allow(dead_code)] fn count_playwright_processes() -> Option<usize> {
let output = Command::new("ps").args(["aux"]).output().ok()?;
let output_str = String::from_utf8(output.stdout).ok()?;
let count = output_str
.lines()
.filter(|line| {
line.contains("playwright") && !line.contains("grep") && !line.contains("test")
})
.count();
Some(count)
}
#[tokio::test]
#[ignore] #[cfg(unix)]
#[cfg(unix)]
async fn test_file_descriptor_cleanup() {
crate::common::init_tracing();
tracing::info!("\n=== Testing File Descriptor Cleanup ===\n");
let initial_fds = count_open_file_descriptors().unwrap_or(0);
tracing::info!("Initial file descriptors: {}", initial_fds);
const CYCLES: usize = 10;
for i in 0..CYCLES {
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let page = browser.new_page().await.expect("Failed to create page");
let _ = page.goto("about:blank", None).await;
let _ = page.close().await;
browser.close().await.expect("Failed to close browser");
tokio::time::sleep(Duration::from_millis(50)).await;
if i % 2 == 1 {
let current_fds = count_open_file_descriptors().unwrap_or(0);
tracing::debug!("After cycle {}: {} FDs", i + 1, current_fds);
}
}
tokio::time::sleep(Duration::from_millis(200)).await;
let final_fds = count_open_file_descriptors().unwrap_or(0);
tracing::info!("\nFinal file descriptors: {}", final_fds);
tracing::info!("FD growth: {}", final_fds as i32 - initial_fds as i32);
let fd_growth = (final_fds as i32 - initial_fds as i32).abs();
assert!(
fd_growth < 20,
"File descriptor leak detected: {} FDs not cleaned up",
fd_growth
);
tracing::info!("\n✓ File descriptors cleaned up properly");
}
#[tokio::test]
#[cfg(unix)]
async fn test_process_cleanup() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Process Cleanup ===\n");
let initial_children = count_child_processes().unwrap_or(0);
tracing::info!("Initial child processes: {}", initial_children);
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
tokio::time::sleep(Duration::from_millis(100)).await;
tokio::time::sleep(Duration::from_millis(100)).await;
let during_children = count_child_processes().unwrap_or(0);
tracing::info!("Child processes during operation: {}", during_children);
drop(playwright);
let start = std::time::Instant::now();
let timeout = Duration::from_secs(2);
let mut final_children = 0;
let mut success = false;
while start.elapsed() < timeout {
final_children = count_child_processes().unwrap_or(0);
if final_children <= initial_children {
success = true;
break;
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
tracing::info!("Final child processes: {}", final_children);
assert!(
success,
"Process leak detected: {} child processes not cleaned up (expected {})",
final_children.saturating_sub(initial_children),
initial_children
);
tracing::info!("\n✓ Child processes cleaned up properly");
}
#[tokio::test]
#[cfg(unix)]
async fn test_no_zombie_processes() {
crate::common::init_tracing();
tracing::info!("\n=== Testing for Zombie Processes ===\n");
fn count_zombies() -> Option<usize> {
let output = Command::new("ps").args(["aux"]).output().ok()?;
let output_str = String::from_utf8(output.stdout).ok()?;
let count = output_str
.lines()
.filter(|line| line.contains("<defunct>") || line.contains("Z"))
.count();
Some(count)
}
let initial_zombies = count_zombies().unwrap_or(0);
tracing::info!("Initial zombies: {}", initial_zombies);
const ZOMBIE_TOLERANCE: usize = 3;
const CYCLES: usize = 5;
for i in 0..CYCLES {
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let page = browser.new_page().await.expect("Failed to create page");
let _ = page.goto("about:blank", None).await;
let _ = page.close().await;
browser.close().await.expect("Failed to close browser");
let start = std::time::Instant::now();
let timeout = Duration::from_secs(2);
let mut current_zombies = 0;
let mut success = false;
let max_allowed = initial_zombies + ZOMBIE_TOLERANCE;
while start.elapsed() < timeout {
current_zombies = count_zombies().unwrap_or(0);
if current_zombies <= max_allowed {
success = true;
break;
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
if i % 2 == 1 {
tracing::debug!("After cycle {}: {} zombies", i + 1, current_zombies);
}
assert!(
success,
"Zombie process leak detected after cycle {}: {} zombies (max allowed: {})",
i + 1,
current_zombies,
max_allowed
);
}
tracing::info!("\n✓ No zombie processes detected");
}
#[tokio::test]
async fn test_multiple_server_cycles() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Multiple Server Launch/Shutdown Cycles ===\n");
const CYCLES: usize = 5;
for i in 0..CYCLES {
tracing::info!("Server cycle {}/{}", i + 1, CYCLES);
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let page = browser.new_page().await.expect("Failed to create page");
let _ = page.goto("about:blank", None).await;
let _ = page.close().await;
browser.close().await.expect("Failed to close browser");
drop(playwright);
tokio::time::sleep(Duration::from_millis(500)).await;
}
tracing::info!("\n✓ Multiple server cycles handled successfully");
}
#[tokio::test]
#[ignore] async fn test_concurrent_browser_cleanup() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Concurrent Browser Cleanup ===\n");
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
const BROWSER_COUNT: usize = 3;
let mut browsers = Vec::new();
for i in 0..BROWSER_COUNT {
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
browsers.push(browser);
tracing::info!("Launched browser {}/{}", i + 1, BROWSER_COUNT);
}
for (i, browser) in browsers.into_iter().enumerate() {
browser.close().await.expect("Failed to close browser");
tracing::info!("Closed browser {}/{}", i + 1, BROWSER_COUNT);
}
tokio::time::sleep(Duration::from_millis(500)).await;
tracing::info!("\n✓ Concurrent browsers cleaned up successfully");
}
#[tokio::test]
#[ignore] async fn test_resource_limit_stress() {
crate::common::init_tracing();
tracing::info!("\n=== Stress Test: Resource Limits ===\n");
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
const PAGE_COUNT: usize = 20;
let mut pages = Vec::new();
for i in 0..PAGE_COUNT {
match browser.new_page().await {
Ok(page) => {
pages.push(page);
if i % 5 == 4 {
tracing::debug!("Created {} pages", i + 1);
}
}
Err(e) => {
tracing::warn!("Failed to create page {}: {:?}", i + 1, e);
break;
}
}
}
tracing::info!("Successfully created {} pages", pages.len());
for (i, page) in pages.into_iter().enumerate() {
let _ = page.close().await;
if i % 5 == 4 {
tracing::debug!("Closed {} pages", i + 1);
}
}
browser.close().await.expect("Failed to close browser");
tracing::info!("\n✓ Resource stress test handled successfully");
}
#[tokio::test]
async fn test_error_quality_element_not_found() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Error Quality: Element Not Found ===\n");
let server = TestServer::start().await;
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let page = browser.new_page().await.expect("Failed to create page");
page.goto(&format!("{}/locators.html", server.url()), None)
.await
.expect("Navigation failed");
let locator = page.locator("button.does-not-exist").await;
let options = ClickOptions {
timeout: Some(1000.0), ..Default::default()
};
let result = locator.click(Some(options)).await;
assert!(result.is_err(), "Expected error for non-existent element");
let error_msg = format!("{:?}", result.unwrap_err());
tracing::info!("Error message: {}", error_msg);
assert!(
error_msg.contains("does-not-exist") || error_msg.contains("button"),
"Error should include selector: {}",
error_msg
);
tracing::info!("\n✓ Element not found error includes selector");
browser.close().await.expect("Failed to close browser");
server.shutdown();
}
#[tokio::test]
async fn test_error_quality_navigation_timeout() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Error Quality: Navigation Timeout ===\n");
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let page = browser.new_page().await.expect("Failed to create page");
let timeout_duration = Duration::from_millis(100);
let target_url = "http://10.255.255.1:9999/page.html";
let options = GotoOptions::new().timeout(timeout_duration);
let result = page.goto(target_url, Some(options)).await;
assert!(result.is_err(), "Expected timeout error");
let error_msg = format!("{:?}", result.unwrap_err());
tracing::info!("Error message: {}", error_msg);
assert!(
error_msg.contains("Timeout") || error_msg.contains("timeout"),
"Error should mention timeout: {}",
error_msg
);
tracing::info!("\n✓ Navigation timeout error includes timeout duration");
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_error_quality_invalid_url() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Error Quality: Invalid URL ===\n");
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let page = browser.new_page().await.expect("Failed to create page");
let invalid_url = "not-a-valid-url";
let result = page.goto(invalid_url, None).await;
assert!(result.is_err(), "Expected error for invalid URL");
let error_msg = format!("{:?}", result.unwrap_err());
tracing::info!("Error message: {}", error_msg);
assert!(!error_msg.is_empty(), "Error message should not be empty");
tracing::info!("\n✓ Invalid URL error is descriptive");
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_error_quality_connection_failed() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Error Quality: Connection Failed ===\n");
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let page = browser.new_page().await.expect("Failed to create page");
let unreachable_url = "http://localhost:59999/";
let result = page.goto(unreachable_url, None).await;
assert!(result.is_err(), "Expected connection error");
let error_msg = format!("{:?}", result.unwrap_err());
tracing::info!("Error message: {}", error_msg);
assert!(!error_msg.is_empty(), "Error message should not be empty");
tracing::info!("\n✓ Connection failed error is descriptive");
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_error_quality_operation_after_close() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Error Quality: Operation After Close ===\n");
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let page = browser.new_page().await.expect("Failed to create page");
page.close().await.expect("Failed to close page");
let result = page.goto("https://example.com", None).await;
assert!(result.is_err(), "Expected error for closed page");
let error_msg = format!("{:?}", result.unwrap_err());
tracing::info!("Error message: {}", error_msg);
assert!(
error_msg.contains("closed") || error_msg.contains("Closed"),
"Error should mention that page is closed: {}",
error_msg
);
tracing::info!("\n✓ Operation after close error is descriptive");
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_error_quality_assertion_timeout() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Error Quality: Assertion Timeout ===\n");
let server = TestServer::start().await;
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let page = browser.new_page().await.expect("Failed to create page");
page.goto(&format!("{}/locators.html", server.url()), None)
.await
.expect("Navigation failed");
let locator = page.locator("button.does-not-exist").await;
let options = ClickOptions {
timeout: Some(1000.0), ..Default::default()
};
let result = locator.click(Some(options)).await;
if let Err(e) = result {
let error_msg = format!("{:?}", e);
tracing::info!("Error message: {}", error_msg);
assert!(!error_msg.is_empty(), "Error message should not be empty");
} else {
tracing::error!("Unexpected success (element should not exist)");
}
tracing::info!("\n✓ Assertion timeout error includes context");
browser.close().await.expect("Failed to close browser");
server.shutdown();
}
#[tokio::test]
async fn test_error_quality_error_sequence() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Error Quality: Multiple Errors ===\n");
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let page = browser.new_page().await.expect("Failed to create page");
let errors = [
("invalid-url", "Invalid URL test"),
("http://localhost:59999/", "Connection refused test"),
("http://10.255.255.1:9999/timeout", "Timeout test"),
];
for (url, test_name) in errors {
let options = GotoOptions::new().timeout(Duration::from_millis(100));
let result = page.goto(url, Some(options)).await;
assert!(result.is_err(), "{} should produce error", test_name);
let error_msg = format!("{:?}", result.unwrap_err());
tracing::info!("{}: {}", test_name, error_msg);
assert!(!error_msg.is_empty(), "Error message should not be empty");
}
tracing::info!("\n✓ Multiple errors are each descriptive");
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_error_quality_audit() {
crate::common::init_tracing();
tracing::info!("\n=== Error Quality Audit ===\n");
tracing::info!("Error Quality Expectations:");
tracing::info!("1. ServerNotFound:");
tracing::info!(" Current: 'Playwright server not found at expected location'");
tracing::info!(
" Improved: 'Playwright server not found. Install with: npm install playwright'"
);
tracing::info!("2. LaunchFailed:");
tracing::info!(" Current: 'Failed to launch Playwright server: <details>'");
tracing::info!(
" Improved: 'Failed to launch Playwright server: <details>. Check that Node.js is installed.'"
);
tracing::info!("3. ElementNotFound:");
tracing::info!(" Current: 'Element not found: <selector>'");
tracing::info!(
" Improved: 'Element not found: <selector>. Waited for <timeout>. Retry with longer timeout or check selector.'"
);
tracing::info!("4. Timeout:");
tracing::info!(" Current: 'Timeout: <message>'");
tracing::info!(
" Improved: 'Timeout after <duration>: <operation> (<url>). Increase timeout or check network.'"
);
tracing::info!("5. TargetClosed:");
tracing::info!(" Current: 'Target closed: <message>'");
tracing::info!(" Improved: 'Target closed: Cannot perform <operation> on closed <target>.'");
tracing::info!("\n✓ Error quality audit documented");
}
#[tokio::test]
async fn test_graceful_shutdown_on_drop() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Graceful Shutdown: Drop Cleanup ===\n");
{
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let page = browser.new_page().await.expect("Failed to create page");
let _ = page.goto("about:blank", None).await;
tracing::info!("Playwright, browser, and page created");
tracing::info!("Dropping all objects...");
drop(page);
drop(browser);
drop(playwright);
}
tokio::time::sleep(Duration::from_secs(1)).await;
tracing::info!("\n✓ Graceful shutdown on drop completed");
}
#[tokio::test]
async fn test_graceful_shutdown_explicit_close() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Graceful Shutdown: Explicit Close ===\n");
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let page = browser.new_page().await.expect("Failed to create page");
let _ = page.goto("about:blank", None).await;
tracing::info!("Closing page...");
page.close().await.expect("Failed to close page");
tracing::info!("Closing browser...");
browser.close().await.expect("Failed to close browser");
tracing::info!("Dropping playwright...");
drop(playwright);
tokio::time::sleep(Duration::from_millis(500)).await;
tracing::info!("\n✓ Explicit close completed successfully");
}
#[tokio::test]
async fn test_graceful_shutdown_multiple_browsers() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Graceful Shutdown: Multiple Browsers ===\n");
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser1 = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser 1");
let browser2 = playwright
.firefox()
.launch()
.await
.expect("Failed to launch browser 2");
tracing::info!("Two browsers launched");
tracing::info!("Closing browser 1...");
browser1.close().await.expect("Failed to close browser 1");
tracing::info!("Closing browser 2...");
browser2.close().await.expect("Failed to close browser 2");
tracing::info!("Dropping playwright...");
drop(playwright);
tokio::time::sleep(Duration::from_millis(500)).await;
tracing::info!("\n✓ Multiple browsers shut down successfully");
}
#[tokio::test]
async fn test_error_recovery_network_timeout() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Error Recovery: Network Timeout ===\n");
let server = TestServer::start().await;
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let page = browser.new_page().await.expect("Failed to create page");
let options = GotoOptions::new().timeout(Duration::from_millis(100));
let result = page.goto("http://10.255.255.1:9999/", Some(options)).await;
assert!(result.is_err(), "Expected timeout error");
assert!(result.is_err(), "Expected timeout error");
tracing::info!("Timeout error occurred (expected)");
let recovery_result = page
.goto(&format!("{}/locators.html", server.url()), None)
.await;
assert!(
recovery_result.is_ok(),
"Page should recover after timeout error: {:?}",
recovery_result
);
tracing::info!("✓ Page recovered after timeout");
browser.close().await.expect("Failed to close browser");
server.shutdown();
}
#[tokio::test]
async fn test_error_recovery_invalid_url() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Error Recovery: Invalid URL ===\n");
let server = TestServer::start().await;
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let page = browser.new_page().await.expect("Failed to create page");
let result = page.goto("not-a-valid-url", None).await;
assert!(result.is_err(), "Expected invalid URL error");
assert!(result.is_err(), "Expected invalid URL error");
tracing::info!("Invalid URL error occurred (expected)");
let recovery_result = page
.goto(&format!("{}/locators.html", server.url()), None)
.await;
assert!(
recovery_result.is_ok(),
"Page should recover after invalid URL error: {:?}",
recovery_result
);
tracing::info!("✓ Page recovered after invalid URL");
browser.close().await.expect("Failed to close browser");
server.shutdown();
}
#[tokio::test]
async fn test_error_recovery_multiple_errors() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Error Recovery: Multiple Errors ===\n");
let server = TestServer::start().await;
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let page = browser.new_page().await.expect("Failed to create page");
let errors = [
"not-valid-url",
"http://localhost:59999/",
"http://10.255.255.1:9999/",
];
for (i, url) in errors.iter().enumerate() {
let options = GotoOptions::new().timeout(Duration::from_millis(100));
let result = page.goto(url, Some(options)).await;
assert!(result.is_err(), "Error {} should fail", i + 1);
assert!(result.is_err(), "Error {} should fail", i + 1);
tracing::info!("Error {} handled (expected)", i + 1);
}
let recovery_result = page
.goto(&format!("{}/locators.html", server.url()), None)
.await;
assert!(
recovery_result.is_ok(),
"Page should recover after multiple errors: {:?}",
recovery_result
);
tracing::info!("✓ Page recovered after multiple consecutive errors");
browser.close().await.expect("Failed to close browser");
server.shutdown();
}
#[tokio::test]
async fn test_error_recovery_page_creation() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Error Recovery: Page Creation ===\n");
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let page1 = browser.new_page().await.expect("Failed to create page 1");
let _ = page1.goto("invalid-url", None).await;
let page2 = browser.new_page().await.expect("Failed to create page 2");
let _ = page2.goto("about:blank", None).await;
assert!(!page2.url().is_empty(), "Page 2 should have a URL");
tracing::info!("✓ Browser recovered and created new page after error");
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_error_recovery_context() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Error Recovery: Context ===\n");
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.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");
let _ = page.goto("invalid-url", None).await;
let page2 = context
.new_page()
.await
.expect("Failed to create second page");
let _ = page2.goto("about:blank", None).await;
assert!(!page2.url().is_empty(), "Page 2 should have a URL");
tracing::info!("✓ Context recovered after page error");
context.close().await.expect("Failed to close context");
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_error_recovery_browser_relaunch() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Error Recovery: Browser Relaunch ===\n");
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser1 = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser 1");
browser1.close().await.expect("Failed to close browser 1");
tracing::info!("Browser 1 closed");
let browser2 = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser 2");
let page = browser2.new_page().await.expect("Failed to create page");
let _ = page.goto("about:blank", None).await;
assert!(!page.url().is_empty(), "Page should have a URL");
tracing::info!("✓ Browser relaunched successfully");
browser2.close().await.expect("Failed to close browser 2");
}
#[tokio::test]
#[ignore] async fn test_error_recovery_stress() {
crate::common::init_tracing();
tracing::info!("\n=== Stress Test: Error Recovery Under Load ===\n");
let server = TestServer::start().await;
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
let page = browser.new_page().await.expect("Failed to create page");
const CYCLES: usize = 10;
let mut successful_navigations = 0;
for i in 0..CYCLES {
if i % 2 == 0 {
let _ = page.goto("http://localhost:59999/", None).await;
tokio::time::sleep(Duration::from_millis(50)).await;
} else {
let result = page
.goto(&format!("{}/locators.html", server.url()), None)
.await;
if result.is_ok() {
successful_navigations += 1;
} else {
tracing::warn!("Navigation failed in cycle {}: {:?}", i, result.err());
}
}
if i % 5 == 4 {
tracing::info!("Completed {} error/success cycles", i + 1);
}
}
let attempts = CYCLES / 2;
tracing::info!(
"Successful navigations: {}/{}",
successful_navigations,
attempts
);
let min_successful = (attempts as f64 * 0.3).ceil() as usize;
assert!(
successful_navigations >= min_successful,
"Too few successful navigations: {} (expected at least {})",
successful_navigations,
min_successful
);
tracing::info!("✓ Error recovery stress test passed");
browser.close().await.expect("Failed to close browser");
server.shutdown();
}
#[tokio::test]
#[cfg(unix)]
async fn test_signal_handling_cleanup() {
crate::common::init_tracing();
tracing::info!("\n=== Testing Signal Handling: Cleanup ===\n");
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let browser = playwright
.chromium()
.launch()
.await
.expect("Failed to launch browser");
drop(browser);
drop(playwright);
tokio::time::sleep(Duration::from_millis(500)).await;
tracing::info!("✓ Cleanup handlers work for signal simulation");
}