use crate::{Desktop, Selector, UIElement};
use std::sync::Arc;
use tokio::runtime::Runtime;
use tracing::Level;
struct AppFixture {
desktop: Arc<Desktop>,
app: Option<UIElement>,
rt: Runtime,
}
impl AppFixture {
fn new(app_name: &str) -> Self {
let rt = Runtime::new().expect("Failed to create Tokio runtime");
let desktop = Arc::new(Desktop::new(false, false).expect("Failed to create Desktop"));
let desktop_clone = desktop.clone();
let app = rt.block_on(async {
match desktop_clone.open_application(app_name) {
Ok(app) => {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
if let Err(e) = app.maximize_window() {
eprintln!("Warning: Could not maximize window for '{app_name}': {e}");
}
Some(app)
}
Err(e) => {
panic!("Failed to open application '{app_name}' for testing: {e}");
}
}
});
AppFixture { desktop, app, rt }
}
}
impl Drop for AppFixture {
fn drop(&mut self) {
if let Some(app) = self.app.take() {
let app_name = app.name().unwrap_or_else(|| "unknown".to_string());
println!("--- Tearing down test, closing '{app_name}' ---");
if let Err(e) = app.close() {
eprintln!("Error closing application '{app_name}' during test teardown: {e}");
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
}
#[test]
fn test_id_selector_finds_element() {
let fixture = AppFixture::new("notepad");
let desktop = fixture.desktop.clone();
let rt = &fixture.rt;
rt.block_on(async {
let edit_selector = Selector::Role {
role: "edit".to_string(),
name: None,
};
let text_area = desktop
.locator(edit_selector)
.first(None)
.await
.expect("Could not find the text area in Notepad.");
let target_id = text_area.id().expect("Text area should have an ID.");
println!("Found text area with ID: {target_id}");
assert!(!target_id.is_empty(), "Element ID should not be empty.");
let id_selector_str = format!("#{target_id}");
let id_selector = Selector::from(id_selector_str.as_str());
let found_element = desktop
.locator(id_selector)
.first(None)
.await
.expect("Failed to find element using ID selector.");
let found_id = found_element
.id()
.expect("Found element should have an ID.");
assert_eq!(
target_id, found_id,
"The ID of the found element must match the target ID."
);
println!("✅ Successfully found element by ID: {target_id}");
});
}
#[test]
fn test_stable_id_across_sessions() {
fn check_id_stability(app_name: &str, selector: Selector, element_description: &str) {
println!("\n--- Testing ID stability for {app_name} ---");
let fixture = AppFixture::new(app_name);
let desktop = fixture.desktop.clone();
let rt = &fixture.rt;
rt.block_on(async {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let element_1 = desktop
.locator(selector.clone())
.first(Some(std::time::Duration::from_secs(5)))
.await
.unwrap_or_else(|e| {
panic!(
"Could not find the {element_description} in {app_name} (first attempt): {e}"
)
});
let id_1 = element_1.id().expect("Element should have an ID.");
println!(
"Found {element_description} in {app_name}. First ID: {id_1}"
);
assert!(!id_1.is_empty(), "First element ID should not be empty.");
let element_2 = desktop
.locator(selector.clone())
.first(Some(std::time::Duration::from_secs(5)))
.await
.unwrap_or_else(|e| {
panic!(
"Could not find the {element_description} in {app_name} (second attempt): {e}"
)
});
let id_2 = element_2.id().expect("Element should have an ID.");
println!(
"Found {element_description} in {app_name} again. Second ID: {id_2}"
);
assert!(!id_2.is_empty(), "Second element ID should not be empty.");
assert_eq!(
id_1, id_2,
"The ID for the {element_description} in {app_name} should be stable across sessions."
);
println!(
"✅ ID for {element_description} in {app_name} is stable."
);
});
}
check_id_stability(
"notepad",
Selector::Role {
role: "edit".to_string(),
name: None,
},
"main text area",
);
check_id_stability(
"ms-settings:",
Selector::Role {
role: "listitem".to_string(),
name: Some("System".to_string()),
},
"'System' button",
);
}
#[test]
fn test_web_id_stability() {
println!("\n--- Testing Web ID stability (Rigorous) ---");
let rt = Runtime::new().expect("Failed to create Tokio runtime");
let desktop = Arc::new(Desktop::new(false, false).expect("Failed to create Desktop"));
rt.block_on(async {
let check_url = |url: String,
element_selector: Selector,
element_description: String,
expected_title_part: String| {
let desktop = desktop.clone();
async move {
println!("-- Checking URL: {url} --");
let app = desktop
.open_url(&url, Some(crate::Browser::Edge))
.expect("Failed to open URL in Edge.");
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
println!("✅ Browser opened to {url}");
let nav_timeout = std::time::Duration::from_secs(20);
let start_time = std::time::Instant::now();
let mut navigated = false;
while start_time.elapsed() < nav_timeout {
if let Some(name) = app.name() {
if name.contains(&expected_title_part) {
println!("✅ Navigation confirmed: Window title is '{name}'");
navigated = true;
break;
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
assert!(
navigated,
"Navigation failed: Window title did not contain '{expected_title_part}' within timeout."
);
let element_1 = app
.locator(element_selector.clone())
.unwrap()
.first(Some(std::time::Duration::from_secs(10)))
.await
.unwrap_or_else(|e| {
panic!(
"Could not find '{element_description}' on {url} (first attempt): {e}"
)
});
let id_1 = element_1.id().expect("Element should have an ID.");
println!("Found '{element_description}'. First ID: {id_1}");
let reload_button = app
.locator(Selector::Role {
role: "button".to_string(),
name: Some("Refresh".to_string()),
})
.unwrap()
.first(None)
.await
.expect("Could not find the 'Reload' button in the browser.");
reload_button
.click()
.expect("Failed to click Reload button.");
println!("Reloaded page, waiting for content to load again...");
let reload_start_time = std::time::Instant::now();
let mut reloaded = false;
while reload_start_time.elapsed() < nav_timeout {
if let Some(name) = app.name() {
if name.contains(&expected_title_part) {
println!("✅ Reload confirmed: Window title is '{name}'");
reloaded = true;
break;
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
assert!(
reloaded,
"Page reload failed: Window title did not contain '{expected_title_part}' after reload."
);
let element_2 = app
.locator(element_selector.clone())
.unwrap()
.first(Some(std::time::Duration::from_secs(10)))
.await
.unwrap_or_else(|e| {
panic!(
"Could not find '{element_description}' on {url} (second attempt, after reload): {e}"
)
});
let id_2 = element_2.id().expect("Element should have a second ID.");
println!("Found '{element_description}' again. Second ID: {id_2}");
assert_eq!(
id_1, id_2,
"Web element ID for '{element_description}' should be stable after a page reload."
);
println!("✅ ID for '{element_description}' is stable on {url}.");
app
}
};
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
let browser_window = Some(
check_url(
"https://lu.ma/airstreet".to_string(),
Selector::Name("Air Street".to_string()),
"Luma event title".to_string(),
"Air Street".to_string(), )
.await,
);
if let Some(window) = browser_window {
println!("--- Tearing down test, closing browser ---");
window.close().expect("Failed to close browser window.");
}
});
}
#[test]
#[ignore = "ID generation is not yet unique across different browser windows (same process), causing this test to fail. Needs investigation into how to uniquely identify windows for hashing."]
fn test_web_id_uniqueness_across_windows() {
tracing::subscriber::set_global_default(
tracing_subscriber::FmtSubscriber::builder()
.with_max_level(Level::DEBUG)
.finish(),
)
.expect("Failed to set tracing subscriber");
println!("\n--- Testing Web ID uniqueness across multiple windows ---");
let rt = Runtime::new().expect("Failed to create Tokio runtime");
let desktop = Arc::new(Desktop::new(false, false).expect("Failed to create Desktop"));
let url = "https://pages.dataiku.com/guide-to-ai-agents";
let element_selector = Selector::Name("Get Ahead With Agentic AI".to_string());
let element_description = "Dataiku page title";
rt.block_on(async {
println!("-- Opening first browser window to {url} --");
let app1 = desktop
.open_url(url, Some(crate::Browser::Edge))
.expect("Failed to open URL in first window.");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
let element1 = app1
.locator(element_selector.clone())
.unwrap()
.first(Some(std::time::Duration::from_secs(10)))
.await
.expect("Could not find element in first window.");
let id1 = element1.id().expect("Element 1 should have an ID.");
println!("Found '{element_description}' in window 1. ID: {id1}");
println!("-- Opening second browser window to {url} --");
let app2 = desktop
.open_url(url, Some(crate::Browser::Edge))
.expect("Failed to open URL in second window.");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
let element2 = app2
.locator(element_selector.clone())
.unwrap()
.first(Some(std::time::Duration::from_secs(10)))
.await
.expect("Could not find element in second window.");
let id2 = element2.id().expect("Element 2 should have an ID.");
println!("Found '{element_description}' in window 2. ID: {id2}");
assert_ne!(
id1, id2,
"IDs for the same element in different browser windows should be different."
);
println!("✅ IDs are different as expected, proving context-awareness.");
println!("--- Tearing down test, closing browser windows ---");
if let Err(e) = app1.close() {
eprintln!(
"Could not close first browser window, it might have been closed already: {e}"
);
}
if let Err(e) = app2.close() {
eprintln!(
"Could not close second browser window, it might have been closed already: {e}"
);
}
});
}