358 lines
13 KiB
Rust
358 lines
13 KiB
Rust
//! UX Test Helper
|
|
//!
|
|
//! High-level interface for UX testing operations
|
|
|
|
use crate::environment::*;
|
|
use std::time::Duration;
|
|
use tokio::time::timeout;
|
|
|
|
/// Main UX test helper providing high-level testing operations
|
|
pub struct UXTestHelper<'a> {
|
|
pub browser: &'a BrowserManager,
|
|
pub api_client: &'a APITestClient,
|
|
pub data_manager: &'a TestDataManager,
|
|
current_user: Option<TestPersona>,
|
|
}
|
|
|
|
impl<'a> UXTestHelper<'a> {
|
|
/// Create a new UX test helper
|
|
pub fn new(environment: &'a UXTestEnvironment) -> Self {
|
|
Self {
|
|
browser: &environment.browser,
|
|
api_client: &environment.api_client,
|
|
data_manager: &environment.data_manager,
|
|
current_user: None,
|
|
}
|
|
}
|
|
|
|
// ===== AUTHENTICATION HELPERS =====
|
|
|
|
/// Register a new user
|
|
pub async fn register_user(&mut self, persona: &TestPersona) -> Result<(), Box<dyn std::error::Error>> {
|
|
log::info!("Registering user: {}", persona.email);
|
|
|
|
// Navigate to registration page
|
|
self.browser.navigate_to("/register").await?;
|
|
self.assert_page_loaded("Register").await?;
|
|
|
|
// Fill registration form
|
|
self.browser.type_text("input[name='email']", &persona.email).await?;
|
|
self.browser.type_text("input[name='password']", &persona.password).await?;
|
|
self.browser.type_text("input[name='name']", &persona.name).await?;
|
|
|
|
// Submit form
|
|
self.browser.click("button[type='submit']").await?;
|
|
|
|
// Wait for success or error
|
|
self.browser.wait_for_element(".alert, .notification, .success").await?;
|
|
|
|
// Take screenshot
|
|
self.browser.take_screenshot(&format!("register_{}", self.safe_filename(&persona.email))).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Login as a test persona
|
|
pub async fn login_as(&mut self, persona: &TestPersona) -> Result<(), Box<dyn std::error::Error>> {
|
|
log::info!("Logging in as: {}", persona.email);
|
|
|
|
// Navigate to login page
|
|
self.browser.navigate_to("/login").await?;
|
|
self.assert_page_loaded("Login").await?;
|
|
|
|
// Fill login form
|
|
self.browser.type_text("input[name='email']", &persona.email).await?;
|
|
self.browser.type_text("input[name='password']", &persona.password).await?;
|
|
|
|
// Submit form
|
|
self.browser.click("button[type='submit']").await?;
|
|
|
|
// Wait for dashboard redirect or success
|
|
self.browser.wait_for_element(".dashboard, .user-menu, .navbar-user").await?;
|
|
|
|
// Verify authentication
|
|
self.assert_user_is_authenticated().await?;
|
|
|
|
// Store current user
|
|
self.current_user = Some(persona.clone());
|
|
|
|
// Take screenshot
|
|
self.browser.take_screenshot(&format!("login_{}", self.safe_filename(&persona.email))).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Logout current user
|
|
pub async fn logout(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
|
log::info!("Logging out current user");
|
|
|
|
// Look for logout link/button
|
|
if self.browser.element_exists(".user-menu").await {
|
|
self.browser.click(".user-menu").await?;
|
|
self.browser.wait_for_element("a[href='/logout']").await?;
|
|
self.browser.click("a[href='/logout']").await?;
|
|
} else {
|
|
// Direct navigation to logout
|
|
self.browser.navigate_to("/logout").await?;
|
|
}
|
|
|
|
// Wait for redirect to home page
|
|
self.browser.wait_for_element(".home, .login-prompt").await?;
|
|
|
|
self.current_user = None;
|
|
|
|
// Take screenshot
|
|
self.browser.take_screenshot("logout").await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ===== SHOPPING HELPERS =====
|
|
|
|
/// Add product to cart
|
|
pub async fn add_product_to_cart(&mut self, product_id: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
log::info!("Adding product to cart: {}", product_id);
|
|
|
|
// Navigate to product or find it in marketplace
|
|
self.browser.navigate_to("/marketplace").await?;
|
|
|
|
// Find the product
|
|
let product_selector = &format!("[data-product-id='{}']", product_id);
|
|
self.browser.wait_for_element(product_selector).await?;
|
|
|
|
// Click add to cart button
|
|
let add_to_cart_selector = &format!("{} .add-to-cart, {} button[data-action='add-to-cart']", product_selector, product_selector);
|
|
self.browser.click(add_to_cart_selector).await?;
|
|
|
|
// Wait for cart update notification
|
|
self.browser.wait_for_element(".cart-updated, .notification, .alert-success").await?;
|
|
|
|
// Take screenshot
|
|
self.browser.take_screenshot(&format!("add_to_cart_{}", product_id)).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Buy product directly (Buy Now)
|
|
pub async fn buy_now(&mut self, product_id: &str) -> Result<String, Box<dyn std::error::Error>> {
|
|
log::info!("Buying product directly: {}", product_id);
|
|
|
|
// Navigate to product
|
|
self.browser.navigate_to("/marketplace").await?;
|
|
|
|
// Find the product
|
|
let product_selector = &format!("[data-product-id='{}']", product_id);
|
|
self.browser.wait_for_element(product_selector).await?;
|
|
|
|
// Click buy now button
|
|
let buy_now_selector = &format!("{} .buy-now, {} button[data-action='buy-now']", product_selector, product_selector);
|
|
self.browser.click(buy_now_selector).await?;
|
|
|
|
// Complete checkout flow
|
|
self.complete_checkout_flow().await
|
|
}
|
|
|
|
/// Complete checkout flow
|
|
pub async fn complete_checkout_flow(&mut self) -> Result<String, Box<dyn std::error::Error>> {
|
|
log::info!("Completing checkout flow");
|
|
|
|
// Wait for checkout page or modal
|
|
self.browser.wait_for_element(".checkout, .checkout-modal, .purchase-confirmation").await?;
|
|
|
|
// If there's a confirm purchase button, click it
|
|
if self.browser.element_exists("button[data-action='confirm-purchase']").await {
|
|
self.browser.click("button[data-action='confirm-purchase']").await?;
|
|
}
|
|
|
|
// Wait for order confirmation
|
|
self.browser.wait_for_element(".order-confirmation, .purchase-success").await?;
|
|
|
|
// Extract order ID if available
|
|
let order_id = if let Ok(order_element) = self.browser.find_element(".order-id, [data-order-id]").await {
|
|
order_element.text().await.unwrap_or_else(|_| "unknown".to_string())
|
|
} else {
|
|
"test-order-id".to_string()
|
|
};
|
|
|
|
// Take screenshot
|
|
self.browser.take_screenshot(&format!("order_confirmation_{}", order_id)).await?;
|
|
|
|
Ok(order_id)
|
|
}
|
|
|
|
/// Navigate to cart
|
|
pub async fn navigate_to_cart(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
|
let cart_url = if self.current_user.is_some() {
|
|
"/dashboard/cart"
|
|
} else {
|
|
"/cart"
|
|
};
|
|
|
|
self.browser.navigate_to(cart_url).await?;
|
|
self.assert_page_loaded("Cart").await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ===== DASHBOARD HELPERS =====
|
|
|
|
/// Navigate to dashboard section
|
|
pub async fn navigate_to_dashboard_section(&mut self, section: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
let url = format!("/dashboard/{}", section);
|
|
self.browser.navigate_to(&url).await?;
|
|
|
|
// Wait for dashboard content
|
|
self.browser.wait_for_element(".dashboard-content, .dashboard-section").await?;
|
|
|
|
// Take screenshot
|
|
self.browser.take_screenshot(&format!("dashboard_{}", section)).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Navigate to wallet
|
|
pub async fn navigate_to_wallet(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
|
self.browser.navigate_to("/dashboard/wallet").await?;
|
|
self.browser.wait_for_element(".wallet-dashboard, .wallet-content").await?;
|
|
|
|
// Take screenshot
|
|
self.browser.take_screenshot("wallet_dashboard").await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Navigate to settings
|
|
pub async fn navigate_to_settings(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
|
self.browser.navigate_to("/dashboard/settings").await?;
|
|
self.browser.wait_for_element(".settings-form, .settings-content").await?;
|
|
|
|
// Take screenshot
|
|
self.browser.take_screenshot("settings_page").await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ===== SETTINGS HELPERS =====
|
|
|
|
/// Update profile information
|
|
pub async fn update_profile(&mut self, name: &str, country: &str, timezone: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
self.navigate_to_settings().await?;
|
|
|
|
// Fill profile form
|
|
self.browser.type_text("input[name='name']", name).await?;
|
|
self.browser.type_text("select[name='country']", country).await?;
|
|
self.browser.type_text("select[name='timezone']", timezone).await?;
|
|
|
|
// Submit form
|
|
self.browser.click("button[data-action='update-profile']").await?;
|
|
|
|
// Wait for success notification
|
|
self.browser.wait_for_element(".alert-success, .notification-success").await?;
|
|
|
|
self.browser.take_screenshot("profile_updated").await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Add SSH key
|
|
pub async fn add_ssh_key(&mut self, key_name: &str, public_key: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
self.navigate_to_settings().await?;
|
|
|
|
// Navigate to SSH keys section
|
|
if self.browser.element_exists("a[href='#ssh-keys']").await {
|
|
self.browser.click("a[href='#ssh-keys']").await?;
|
|
}
|
|
|
|
// Fill SSH key form
|
|
self.browser.type_text("input[name='ssh_key_name']", key_name).await?;
|
|
self.browser.type_text("textarea[name='public_key']", public_key).await?;
|
|
|
|
// Submit form
|
|
self.browser.click("button[data-action='add-ssh-key']").await?;
|
|
|
|
// Wait for success notification
|
|
self.browser.wait_for_element(".alert-success, .notification-success").await?;
|
|
|
|
self.browser.take_screenshot(&format!("ssh_key_added_{}", key_name)).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ===== ASSERTION HELPERS =====
|
|
|
|
/// Assert that page has loaded with expected title
|
|
pub async fn assert_page_loaded(&self, expected_title_part: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
let title = self.browser.get_title().await?;
|
|
if !title.to_lowercase().contains(&expected_title_part.to_lowercase()) {
|
|
return Err(format!("Expected page title to contain '{}', got '{}'", expected_title_part, title).into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Assert user is authenticated
|
|
pub async fn assert_user_is_authenticated(&self) -> Result<(), Box<dyn std::error::Error>> {
|
|
// Check for authentication indicators in UI
|
|
if !self.browser.element_exists(".user-menu, .navbar-user, .dashboard-link").await {
|
|
return Err("User does not appear to be authenticated (no user menu found)".into());
|
|
}
|
|
|
|
// Validate with API
|
|
let auth_status = self.api_client.validate_auth_status().await?;
|
|
if !auth_status.authenticated {
|
|
return Err("API reports user is not authenticated".into());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Assert cart item count
|
|
pub async fn assert_cart_item_count(&self, expected_count: usize) -> Result<(), Box<dyn std::error::Error>> {
|
|
// Check UI cart badge
|
|
if let Ok(cart_badge) = self.browser.find_element(".cart-badge, .cart-count").await {
|
|
let displayed_count = cart_badge.text().await?.parse::<usize>().unwrap_or(0);
|
|
if displayed_count != expected_count {
|
|
return Err(format!("Expected cart count {}, but UI shows {}", expected_count, displayed_count).into());
|
|
}
|
|
}
|
|
|
|
// Validate with API
|
|
let cart_state = self.api_client.validate_cart_state().await?;
|
|
if cart_state.item_count != expected_count {
|
|
return Err(format!("Expected cart count {}, but API shows {}", expected_count, cart_state.item_count).into());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Assert element contains text
|
|
pub async fn assert_element_contains_text(&self, selector: &str, expected_text: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
let element_text = self.browser.get_text(selector).await?;
|
|
if !element_text.contains(expected_text) {
|
|
return Err(format!("Expected element '{}' to contain '{}', but got '{}'", selector, expected_text, element_text).into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// ===== UTILITY HELPERS =====
|
|
|
|
/// Create safe filename from email
|
|
fn safe_filename(&self, email: &str) -> String {
|
|
email.replace("@", "_at_").replace(".", "_dot_")
|
|
}
|
|
|
|
/// Take screenshot with timestamp
|
|
pub async fn take_screenshot(&self, name: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
self.browser.take_screenshot(name).await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Wait for specific duration
|
|
pub async fn wait(&self, duration: Duration) {
|
|
tokio::time::sleep(duration).await;
|
|
}
|
|
|
|
/// Get current user
|
|
pub fn current_user(&self) -> Option<&TestPersona> {
|
|
self.current_user.as_ref()
|
|
}
|
|
} |