bottest/tests/e2e/platform_flow.rs
Rodrigo Rodriguez (Pragmatismo) de58bc16a0 Add comprehensive E2E testing framework with platform flow
- Create platform_flow.rs with complete user journey test (load → botserver → login → chat → logout)
- Add TEMP_STACK_SETUP.md guide for using temporary isolated stacks
- Add E2E_TESTING_PLAN.md with architecture and implementation strategy
- Update e2e mod.rs to include new platform_flow module
- Include helpers for: platform loading, botserver verification, login, chat, logout
- Support for headless and headed browser testing
- Environment variable configuration for WebDriver, timeouts, logging
2025-12-06 11:15:14 -03:00

418 lines
12 KiB
Rust

//! Complete E2E test for General Bots platform flow
//!
//! Tests the full user journey:
//! 1. Platform loading (UI assets)
//! 2. BotServer initialization
//! 3. User login
//! 4. Chat interaction
//! 5. User logout
use bottest::prelude::*;
use bottest::web::{Browser, BrowserConfig};
use std::time::Duration;
use super::{browser_config, check_webdriver_available, should_run_e2e_tests, E2ETestContext};
/// Step 1: Verify platform loads
/// - Check UI is served
/// - Verify health endpoint responds
/// - Confirm database migrations completed
pub async fn verify_platform_loading(ctx: &E2ETestContext) -> anyhow::Result<()> {
let client = reqwest::Client::new();
// Check health endpoint
let health_url = format!("{}/health", ctx.base_url());
let health_resp = client.get(&health_url).send().await?;
assert!(
health_resp.status().is_success(),
"Health check failed with status: {}",
health_resp.status()
);
println!("✓ Platform health check passed");
// Verify API is responsive
let api_url = format!("{}/api/v1", ctx.base_url());
let api_resp = client.get(&api_url).send().await?;
assert!(
api_resp.status().is_success() || api_resp.status().as_u16() == 401,
"API endpoint failed with status: {}",
api_resp.status()
);
println!("✓ Platform API responsive");
Ok(())
}
/// Step 2: Verify BotServer is running and initialized
/// - Check service discovery
/// - Verify configuration loaded
/// - Confirm database connection
pub async fn verify_botserver_running(ctx: &E2ETestContext) -> anyhow::Result<()> {
let client = reqwest::Client::new();
// Check if server is actually running
assert!(ctx.server.is_running(), "BotServer process is not running");
println!("✓ BotServer process running");
// Verify server info endpoint
let info_url = format!("{}/api/v1/server/info", ctx.base_url());
match client.get(&info_url).send().await {
Ok(resp) => {
if resp.status().is_success() {
let body = resp.text().await?;
assert!(!body.is_empty(), "Server info response is empty");
println!(
"✓ BotServer initialized with info: {}",
body.chars().take(100).collect::<String>()
);
} else {
println!(
"⚠ Server info endpoint returned {}, continuing anyway",
resp.status()
);
}
}
Err(e) => {
println!(
"⚠ Could not reach server info endpoint: {}, continuing anyway",
e
);
}
}
println!("✓ BotServer is running and initialized");
Ok(())
}
/// Step 3: User login flow
/// - Navigate to login page
/// - Enter test credentials
/// - Verify session created
/// - Confirm redirect to dashboard
pub async fn test_user_login(browser: &Browser, ctx: &E2ETestContext) -> anyhow::Result<()> {
let login_url = format!("{}/login", ctx.base_url());
// Navigate to login page
browser.navigate(&login_url).await?;
println!("✓ Navigated to login page: {}", login_url);
// Wait for login form to be visible
let timeout = Duration::from_secs(10);
browser
.wait_for_element("input[type='email']", timeout)
.await?;
println!("✓ Login form loaded");
// Fill in test credentials
let test_email = "test@example.com";
let test_password = "TestPassword123!";
browser
.fill_input("input[type='email']", test_email)
.await?;
println!("✓ Entered email: {}", test_email);
browser
.fill_input("input[type='password']", test_password)
.await?;
println!("✓ Entered password");
// Submit login form
let submit_timeout = Duration::from_secs(5);
browser
.click("button[type='submit']", submit_timeout)
.await?;
println!("✓ Clicked login button");
// Wait for redirect or dashboard
let redirect_timeout = Duration::from_secs(15);
let current_url = browser.get_current_url(redirect_timeout).await?;
// Check we're not on login page anymore
assert!(
!current_url.contains("/login"),
"Still on login page after login attempt. URL: {}",
current_url
);
println!("✓ Redirected from login page to: {}", current_url);
// Verify we can see dashboard or chat area
browser
.wait_for_element(
"[data-testid='chat-area'], [data-testid='dashboard'], main",
Duration::from_secs(10),
)
.await?;
println!("✓ Dashboard or chat area visible");
Ok(())
}
/// Step 4: Chat interaction
/// - Open chat window
/// - Send test message
/// - Receive bot response
/// - Verify message persisted
pub async fn test_chat_interaction(browser: &Browser, ctx: &E2ETestContext) -> anyhow::Result<()> {
// Ensure we're on chat page
let chat_url = format!("{}/chat", ctx.base_url());
browser.navigate(&chat_url).await?;
println!("✓ Navigated to chat page");
// Wait for chat interface to load
browser
.wait_for_element(
"[data-testid='message-input'], textarea.chat-input, input.message",
Duration::from_secs(10),
)
.await?;
println!("✓ Chat interface loaded");
// Send test message
let test_message = "Hello, I need help";
browser
.fill_input("textarea.chat-input, input.message", test_message)
.await?;
println!("✓ Typed message: {}", test_message);
// Click send button or press Enter
let send_result = browser
.click(
"button[data-testid='send-button'], button.send-btn",
Duration::from_secs(5),
)
.await;
if send_result.is_err() {
// Try pressing Enter as alternative
browser.press_key("Enter").await?;
println!("✓ Sent message with Enter key");
} else {
println!("✓ Clicked send button");
}
// Wait for message to appear in chat history
browser
.wait_for_element(
"[data-testid='message-item'], .message-bubble, [class*='message']",
Duration::from_secs(10),
)
.await?;
println!("✓ Message appeared in chat");
// Wait for bot response
let response_timeout = Duration::from_secs(30);
browser
.wait_for_element(
"[data-testid='bot-response'], .bot-message, [class*='bot']",
response_timeout,
)
.await?;
println!("✓ Received bot response");
// Get response text
let response_text = browser
.get_text("[data-testid='bot-response'], .bot-message, [class*='bot']")
.await
.ok();
if let Some(text) = response_text {
println!(
"✓ Bot response: {}",
text.chars().take(100).collect::<String>()
);
}
Ok(())
}
/// Step 5: User logout flow
/// - Click logout button
/// - Verify session invalidated
/// - Confirm redirect to login
/// - Verify cannot access protected routes
pub async fn test_user_logout(browser: &Browser, ctx: &E2ETestContext) -> anyhow::Result<()> {
// Find and click logout button
let logout_selectors = vec![
"button[data-testid='logout-btn']",
"button.logout",
"[data-testid='user-menu'] button[data-testid='logout']",
"a[href*='logout']",
];
let mut logout_found = false;
for selector in logout_selectors {
if let Ok(_) = browser.click(selector, Duration::from_secs(3)).await {
println!("✓ Clicked logout button: {}", selector);
logout_found = true;
break;
}
}
if !logout_found {
println!("⚠ Could not find logout button, attempting with keyboard shortcut");
browser.press_key("l").await.ok(); // Some apps use 'l' for logout
}
// Wait for redirect to login
let redirect_timeout = Duration::from_secs(10);
let current_url = browser.get_current_url(redirect_timeout).await?;
assert!(
current_url.contains("/login") || current_url.contains("/auth"),
"Not redirected to login page after logout. URL: {}",
current_url
);
println!("✓ Redirected to login page after logout: {}", current_url);
// Verify we cannot access protected routes
let chat_url = format!("{}/chat", ctx.base_url());
browser.navigate(&chat_url).await?;
let check_url = browser.get_current_url(Duration::from_secs(5)).await?;
assert!(
check_url.contains("/login") || check_url.contains("/auth"),
"Should be redirected to login when accessing protected route after logout. URL: {}",
check_url
);
println!("✓ Protected routes properly redirect to login");
Ok(())
}
/// Complete platform flow test
///
/// This test validates the entire user journey:
/// 1. Platform loads successfully
/// 2. BotServer is initialized and running
/// 3. User can login with credentials
/// 4. User can interact with chat
/// 5. User can logout and lose access
#[tokio::test]
async fn test_complete_platform_flow_login_chat_logout() {
if !should_run_e2e_tests() {
eprintln!("Skipping: E2E tests disabled (set SKIP_E2E_TESTS env var to disable)");
return;
}
if !check_webdriver_available().await {
eprintln!("Skipping: WebDriver not available at configured URL");
return;
}
println!("\n=== Starting Complete Platform Flow Test ===\n");
// Setup context
let ctx = match E2ETestContext::setup_with_browser().await {
Ok(ctx) => ctx,
Err(e) => {
eprintln!("Failed to setup E2E context: {}", e);
return;
}
};
if !ctx.has_browser() {
eprintln!("Browser not available");
return;
}
let browser = ctx.browser.as_ref().unwrap();
// Test each phase
println!("\n--- Phase 1: Platform Loading ---");
if let Err(e) = verify_platform_loading(&ctx).await {
eprintln!("Platform loading test failed: {}", e);
return;
}
println!("\n--- Phase 2: BotServer Initialization ---");
if let Err(e) = verify_botserver_running(&ctx).await {
eprintln!("BotServer initialization test failed: {}", e);
return;
}
println!("\n--- Phase 3: User Login ---");
if let Err(e) = test_user_login(browser, &ctx).await {
eprintln!("Login test failed: {}", e);
return;
}
println!("\n--- Phase 4: Chat Interaction ---");
if let Err(e) = test_chat_interaction(browser, &ctx).await {
eprintln!("Chat interaction test failed: {}", e);
// Don't return here - try to logout anyway
}
println!("\n--- Phase 5: User Logout ---");
if let Err(e) = test_user_logout(browser, &ctx).await {
eprintln!("Logout test failed: {}", e);
return;
}
println!("\n=== Complete Platform Flow Test PASSED ===\n");
ctx.close().await;
}
/// Simpler test for basic platform loading without browser
#[tokio::test]
async fn test_platform_loading_http_only() {
if !should_run_e2e_tests() {
eprintln!("Skipping: E2E tests disabled");
return;
}
println!("\n=== Testing Platform Loading (HTTP Only) ===\n");
let ctx = match E2ETestContext::setup().await {
Ok(ctx) => ctx,
Err(e) => {
eprintln!("Failed to setup context: {}", e);
return;
}
};
if let Err(e) = verify_platform_loading(&ctx).await {
eprintln!("Platform loading failed: {}", e);
return;
}
println!("\n✓ Platform Loading Test PASSED\n");
ctx.close().await;
}
/// Test BotServer startup and health
#[tokio::test]
async fn test_botserver_startup() {
if !should_run_e2e_tests() {
eprintln!("Skipping: E2E tests disabled");
return;
}
println!("\n=== Testing BotServer Startup ===\n");
let ctx = match E2ETestContext::setup().await {
Ok(ctx) => ctx,
Err(e) => {
eprintln!("Failed to setup context: {}", e);
return;
}
};
if let Err(e) = verify_botserver_running(&ctx).await {
eprintln!("BotServer test failed: {}", e);
return;
}
println!("\n✓ BotServer Startup Test PASSED\n");
ctx.close().await;
}