316 lines
8.4 KiB
Rust
316 lines
8.4 KiB
Rust
mod accessibility;
|
|
|
|
use bottest::prelude::*;
|
|
use reqwest::Client;
|
|
use serde_json::json;
|
|
use std::time::Duration;
|
|
|
|
fn test_client() -> Client {
|
|
Client::builder()
|
|
.timeout(Duration::from_secs(30))
|
|
.build()
|
|
.expect("Failed to create HTTP client")
|
|
}
|
|
|
|
fn external_server_url() -> Option<String> {
|
|
std::env::var("BOTSERVER_URL").ok()
|
|
}
|
|
|
|
async fn get_test_server() -> Option<(Option<TestContext>, String)> {
|
|
if let Some(url) = external_server_url() {
|
|
let client = reqwest::Client::builder()
|
|
.timeout(Duration::from_secs(2))
|
|
.build()
|
|
.ok()?;
|
|
|
|
if client.get(&url).send().await.is_ok() {
|
|
return Some((None, url));
|
|
}
|
|
}
|
|
|
|
let ctx = TestHarness::quick().await.ok()?;
|
|
let server = ctx.start_botserver().await.ok()?;
|
|
|
|
if server.is_running() {
|
|
Some((Some(ctx), server.url.clone()))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
macro_rules! skip_if_no_server {
|
|
($base_url:expr) => {
|
|
if $base_url.is_none() {
|
|
eprintln!("Skipping test: no server available");
|
|
return;
|
|
}
|
|
};
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_page_has_title() {
|
|
let server = get_test_server().await;
|
|
skip_if_no_server!(server);
|
|
|
|
let (_ctx, base_url) = server.unwrap();
|
|
let client = test_client();
|
|
|
|
let response = client
|
|
.get(format!("{}/suite/auth/login.html", base_url))
|
|
.send()
|
|
.await;
|
|
|
|
match response {
|
|
Ok(resp) => {
|
|
if resp.status().is_success() {
|
|
let body = resp.text().await.unwrap_or_default();
|
|
assert!(
|
|
body.contains("<title>") || body.contains("<Title>"),
|
|
"Page should have a title element"
|
|
);
|
|
}
|
|
}
|
|
Err(_) => {
|
|
eprintln!("Skipping: server not available");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_form_labels_present() {
|
|
let server = get_test_server().await;
|
|
skip_if_no_server!(server);
|
|
|
|
let (_ctx, base_url) = server.unwrap();
|
|
let client = test_client();
|
|
|
|
let response = client
|
|
.get(format!("{}/suite/auth/login.html", base_url))
|
|
.send()
|
|
.await;
|
|
|
|
match response {
|
|
Ok(resp) => {
|
|
if resp.status().is_success() {
|
|
let body = resp.text().await.unwrap_or_default();
|
|
|
|
let has_label = body.contains("<label") || body.contains("aria-label");
|
|
let has_input = body.contains("<input");
|
|
|
|
if has_input {
|
|
assert!(
|
|
has_label,
|
|
"Form inputs should have associated labels"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Err(_) => {
|
|
eprintln!("Skipping: server not available");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_button_has_text() {
|
|
let server = get_test_server().await;
|
|
skip_if_no_server!(server);
|
|
|
|
let (_ctx, base_url) = server.unwrap();
|
|
let client = test_client();
|
|
|
|
let response = client
|
|
.get(format!("{}/suite/auth/login.html", base_url))
|
|
.send()
|
|
.await;
|
|
|
|
match response {
|
|
Ok(resp) => {
|
|
if resp.status().is_success() {
|
|
let body = resp.text().await.unwrap_or_default();
|
|
|
|
let has_buttons = body.contains("<button");
|
|
|
|
if has_buttons {
|
|
let has_button_text = body.contains("</button>");
|
|
assert!(
|
|
has_button_text,
|
|
"Buttons should have accessible text"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Err(_) => {
|
|
eprintln!("Skipping: server not available");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_images_have_alt_text() {
|
|
let server = get_test_server().await;
|
|
skip_if_no_server!(server);
|
|
|
|
let (_ctx, base_url) = server.unwrap();
|
|
let client = test_client();
|
|
|
|
let response = client
|
|
.get(format!("{}/suite/auth/login.html", base_url))
|
|
.send()
|
|
.await;
|
|
|
|
match response {
|
|
Ok(resp) => {
|
|
if resp.status().is_success() {
|
|
let body = resp.text().await.unwrap_or_default();
|
|
|
|
let has_images = body.contains("<img");
|
|
|
|
if has_images {
|
|
let has_alt = body.contains("alt=");
|
|
assert!(
|
|
has_alt,
|
|
"Images should have alt attribute for accessibility"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Err(_) => {
|
|
eprintln!("Skipping: server not available");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_headings_have_proper_order() {
|
|
let server = get_test_server().await;
|
|
skip_if_no_server!(server);
|
|
|
|
let (_ctx, base_url) = server.unwrap();
|
|
let client = test_client();
|
|
|
|
let response = client
|
|
.get(format!("{}/suite/auth/login.html", base_url))
|
|
.send()
|
|
.await;
|
|
|
|
match response {
|
|
Ok(resp) => {
|
|
if resp.status().is_success() {
|
|
let body = resp.text().await.unwrap_or_default();
|
|
|
|
let h1_count = body.matches("<h1").count();
|
|
let h2_count = body.matches("<h2").count();
|
|
|
|
if h1_count > 1 {
|
|
eprintln!("Note: Multiple h1 elements found - consider using single h1");
|
|
}
|
|
|
|
if h2_count > 0 && h1_count == 0 {
|
|
eprintln!("Note: Page has h2 but no h1 - heading structure may be suboptimal");
|
|
}
|
|
}
|
|
}
|
|
Err(_) => {
|
|
eprintln!("Skipping: server not available");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_color_contrast() {
|
|
let server = get_test_server().await;
|
|
skip_if_no_server!(server);
|
|
|
|
let (_ctx, base_url) = server.unwrap();
|
|
let client = test_client();
|
|
|
|
let response = client
|
|
.get(format!("{}/suite/auth/login.html", base_url))
|
|
.send()
|
|
.await;
|
|
|
|
match response {
|
|
Ok(resp) => {
|
|
if resp.status().is_success() {
|
|
let body = resp.text().await.unwrap_or_default();
|
|
|
|
assert!(
|
|
body.contains("color:") || body.contains("background") || body.contains("class="),
|
|
"Page should have color styling for visual accessibility"
|
|
);
|
|
|
|
eprintln!("Note: Color contrast testing requires visual analysis or automated tools");
|
|
}
|
|
}
|
|
Err(_) => {
|
|
eprintln!("Skipping: server not available");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_keyboard_navigation() {
|
|
let server = get_test_server().await;
|
|
skip_if_no_server!(server);
|
|
|
|
let (_ctx, base_url) = server.unwrap();
|
|
let client = test_client();
|
|
|
|
let response = client
|
|
.get(format!("{}/suite/auth/login.html", base_url))
|
|
.send()
|
|
.await;
|
|
|
|
match response {
|
|
Ok(resp) => {
|
|
if resp.status().is_success() {
|
|
let body = resp.text().await.unwrap_or_default();
|
|
|
|
let has_focusable = body.contains("tabindex=") || body.contains("href=");
|
|
|
|
assert!(
|
|
has_focusable,
|
|
"Page should have keyboard-navigable elements"
|
|
);
|
|
}
|
|
}
|
|
Err(_) => {
|
|
eprintln!("Skipping: server not available");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_error_messages_accessible() {
|
|
let server = get_test_server().await;
|
|
skip_if_no_server!(server);
|
|
|
|
let (_ctx, base_url) = server.unwrap();
|
|
let client = test_client();
|
|
|
|
let response = client
|
|
.post(format!("{}/api/auth/login", base_url))
|
|
.json(&json!({
|
|
"username": "",
|
|
"password": ""
|
|
}))
|
|
.send()
|
|
.await;
|
|
|
|
match response {
|
|
Ok(resp) => {
|
|
if resp.status() == 400 || resp.status() == 422 {
|
|
let body = resp.text().await.unwrap_or_default();
|
|
|
|
assert!(
|
|
body.len() > 0,
|
|
"Error responses should provide accessible error messages"
|
|
);
|
|
}
|
|
}
|
|
Err(_) => {
|
|
eprintln!("Skipping: server not available");
|
|
}
|
|
}
|
|
}
|