use image::GenericImageView;
use playwright::{
api::{BrowserContext, Page},
Playwright,
};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU32, Ordering};
static SCREENSHOT_COUNTER: AtomicU32 = AtomicU32::new(0);
pub struct BrowserHelpers;
impl BrowserHelpers {
pub async fn set_up_browser(
user_count: usize,
) -> Result<(Playwright, Vec<(BrowserContext, Page)>), Box<dyn std::error::Error>> {
let playwright = Playwright::initialize().await?;
let headless = std::env::var("BROWSER_VISIBLE").is_err();
if !headless {
println!(
"🌐 Running browser in VISIBLE mode for debugging with {} users",
user_count
);
}
let chromium = playwright.chromium();
let browser = Self::try_launch_browser(&chromium, headless).await?;
let mut contexts_and_pages = Vec::new();
for i in 0..user_count {
let context = browser
.context_builder()
.accept_downloads(true)
.build()
.await?;
let page = context.new_page().await?;
if !headless {
println!("📱 Created browser context for User {}", i + 1);
}
contexts_and_pages.push((context, page));
}
Ok((playwright, contexts_and_pages))
}
async fn try_launch_browser(
chromium: &playwright::api::BrowserType,
headless: bool,
) -> Result<playwright::api::Browser, Box<dyn std::error::Error>> {
match chromium.launcher().headless(headless).launch().await {
Ok(browser) => return Ok(browser),
Err(_) => println!(
"Preinstalled Playwright not available on this platform, trying fallbacks..."
),
}
let browser_paths = if cfg!(target_os = "macos") {
vec![
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
]
} else if cfg!(target_os = "linux") {
vec![
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
"/usr/bin/chromium-browser",
"/usr/bin/chromium",
]
} else {
vec![]
};
for path in &browser_paths {
if std::path::Path::new(path).exists() {
println!("Trying system browser at: {}", path);
match chromium
.launcher()
.headless(headless)
.executable(std::path::Path::new(path))
.launch()
.await
{
Ok(browser) => return Ok(browser),
Err(_) => {} }
}
}
Err("No working browser found. Tried Playwright-installed and system browsers.".into())
}
pub async fn screenshot_compare(
page: &Page,
test_name: &str,
selectors_to_grey: &[&str],
) -> Result<(), Box<dyn std::error::Error>> {
let counter = SCREENSHOT_COUNTER.fetch_add(1, Ordering::Relaxed) + 1;
let prefixed_name = format!("{:03}_{}", counter, test_name);
let screenshots_dir = "tests/screenshots";
std::fs::create_dir_all(screenshots_dir)?;
let reference_path = format!("{}/{}_reference.png", screenshots_dir, prefixed_name);
let current_path = format!("{}/{}_current.png", screenshots_dir, prefixed_name);
let diff_path = format!("{}/{}_diff.png", screenshots_dir, prefixed_name);
for selector in selectors_to_grey {
let script = format!(
r#"
document.querySelectorAll('{}').forEach(el => {{
el.style.background = '#f0f0f0';
el.style.color = '#888888';
el.style.borderColor = '#cccccc';
}});
"#,
selector
);
if let Err(e) = page
.evaluate::<serde_json::Value, serde_json::Value>(&script, serde_json::Value::Null)
.await
{
println!("Warning: Could not grey out selector '{}': {}", selector, e);
}
}
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
page.screenshot_builder()
.path(PathBuf::from(¤t_path))
.full_page(true)
.screenshot()
.await?;
if !Path::new(&reference_path).exists() {
std::fs::copy(¤t_path, &reference_path)?;
println!("📸 Created reference screenshot for '{}'", prefixed_name);
return Ok(());
}
let current_img = image::open(¤t_path)?;
let reference_img = image::open(&reference_path)?;
if current_img.dimensions() != reference_img.dimensions() {
let error_msg = format!(
"Screenshot '{}' dimensions differ: current {:?} vs reference {:?}",
prefixed_name,
current_img.dimensions(),
reference_img.dimensions()
);
println!("⚠ {}", error_msg);
return Err(error_msg.into());
}
let current_rgba = current_img.to_rgba8();
let reference_rgba = reference_img.to_rgba8();
let mut diff_pixels = 0u32;
for (current_pixel, reference_pixel) in current_rgba.pixels().zip(reference_rgba.pixels()) {
let diff = ((current_pixel[0] as i32 - reference_pixel[0] as i32).abs()
+ (current_pixel[1] as i32 - reference_pixel[1] as i32).abs()
+ (current_pixel[2] as i32 - reference_pixel[2] as i32).abs())
as u32;
if diff > 30 {
diff_pixels += 1;
}
}
let total_pixels = current_rgba.pixels().len() as u32;
let diff_percentage = (diff_pixels as f64 / total_pixels as f64) * 100.0;
if diff_percentage > 5.0 {
let mut diff_buffer = current_rgba.clone();
for (i, (current_pixel, reference_pixel)) in current_rgba
.pixels()
.zip(reference_rgba.pixels())
.enumerate()
{
let diff = ((current_pixel[0] as i32 - reference_pixel[0] as i32).abs()
+ (current_pixel[1] as i32 - reference_pixel[1] as i32).abs()
+ (current_pixel[2] as i32 - reference_pixel[2] as i32).abs())
as u32;
if diff > 30 {
let y = i as u32 / current_rgba.width();
let x = i as u32 % current_rgba.width();
diff_buffer.put_pixel(x, y, image::Rgba([255, 0, 0, 255])); }
}
diff_buffer.save(&diff_path)?;
let error_msg = format!(
"Screenshot '{}' differs from reference by {:.2}% - diff saved to {}",
prefixed_name, diff_percentage, diff_path
);
println!("⚠ {}", error_msg);
Err(error_msg.into())
} else {
println!(
"✓ Screenshot '{}' matches reference (difference: {:.2}%)",
prefixed_name, diff_percentage
);
let _ = std::fs::remove_file(¤t_path);
Ok(())
}
}
}