Files
leptos-shadcn-ui/packages/leptos/input/src/tests.rs
Peter Hanssens 7a36292cf9 🚀 Release v0.1.0: WASM-compatible components with tailwind-rs-core v0.4.0
- Fixed compilation errors in menubar, combobox, and drawer packages
- Updated to tailwind-rs-core v0.4.0 and tailwind-rs-wasm v0.4.0 for WASM compatibility
- Cleaned up unused variable warnings across packages
- Updated release documentation with WASM integration details
- Demo working with dynamic color API and Tailwind CSS generation
- All 25+ core components ready for crates.io publication

Key features:
 WASM compatibility (no more tokio/mio dependencies)
 Dynamic Tailwind CSS class generation
 Type-safe color utilities
 Production-ready component library
2025-09-16 08:36:13 +10:00

445 lines
16 KiB
Rust

#[cfg(test)]
mod tests {
use crate::default::{Input, INPUT_CLASS};
use leptos::prelude::*;
use std::sync::{Arc, Mutex};
// ============================================================================
// TDD PATTERN 1: RED - Write failing tests first
// ============================================================================
#[test]
fn test_input_validation_required_field() {
// RED: This test should fail initially - we need to add validation support
let validation_state = RwSignal::new(ValidationState::new());
let is_required = Signal::derive(|| true);
let value = RwSignal::new("".to_string());
// Test that empty required field triggers validation error
let is_valid = Signal::derive(move || {
if is_required.get() && value.get().trim().is_empty() {
false
} else {
true
}
});
assert!(!is_valid.get(), "Required field should be invalid when empty");
value.set("valid input".to_string());
assert!(is_valid.get(), "Required field should be valid when filled");
}
#[test]
fn test_input_validation_email_format() {
// RED: Email validation should fail for invalid formats
let email_value = RwSignal::new("invalid-email".to_string());
let is_valid_email = Signal::derive(move || {
let email = email_value.get();
email.contains('@') && email.contains('.') && email.len() > 5
});
assert!(!is_valid_email.get(), "Invalid email format should fail validation");
email_value.set("user@example.com".to_string());
assert!(is_valid_email.get(), "Valid email format should pass validation");
}
#[test]
fn test_input_validation_min_length() {
// RED: Minimum length validation
let value = RwSignal::new("ab".to_string());
let min_length = 3;
let is_valid_length = Signal::derive(move || {
value.get().len() >= min_length
});
assert!(!is_valid_length.get(), "Value below minimum length should be invalid");
value.set("abc".to_string());
assert!(is_valid_length.get(), "Value meeting minimum length should be valid");
}
#[test]
fn test_input_validation_max_length() {
// RED: Maximum length validation
let value = RwSignal::new("very long input that exceeds limit".to_string());
let max_length = 10;
let is_valid_length = Signal::derive(move || {
value.get().len() <= max_length
});
assert!(!is_valid_length.get(), "Value exceeding maximum length should be invalid");
value.set("short".to_string());
assert!(is_valid_length.get(), "Value within maximum length should be valid");
}
#[test]
fn test_input_validation_pattern_matching() {
// RED: Pattern validation (e.g., phone number, alphanumeric)
let value = RwSignal::new("abc123!@#".to_string());
let pattern = regex::Regex::new(r"^[a-zA-Z0-9]+$").unwrap();
let is_valid_pattern = Signal::derive(move || {
pattern.is_match(&value.get())
});
assert!(!is_valid_pattern.get(), "Value with special characters should fail pattern validation");
value.set("abc123".to_string());
assert!(is_valid_pattern.get(), "Alphanumeric value should pass pattern validation");
}
#[test]
fn test_input_validation_error_display() {
// RED: Error message display functionality
let validation_error = RwSignal::new(Some("This field is required".to_string()));
let show_error = Signal::derive(move || validation_error.get().is_some());
assert!(show_error.get(), "Error should be shown when validation fails");
assert_eq!(validation_error.get().unwrap(), "This field is required");
validation_error.set(None);
assert!(!show_error.get(), "Error should be hidden when validation passes");
}
#[test]
fn test_input_validation_real_time_feedback() {
// RED: Real-time validation as user types
let value = RwSignal::new("".to_string());
let validation_errors = RwSignal::new(Vec::<String>::new());
let validate_input = move || {
let mut errors = Vec::new();
let current_value = value.get();
if current_value.trim().is_empty() {
errors.push("Field is required".to_string());
}
if current_value.len() < 3 {
errors.push("Must be at least 3 characters".to_string());
}
validation_errors.set(errors);
};
// Initial state - should have errors
validate_input();
assert!(!validation_errors.get().is_empty());
// Partial input - should still have length error
value.set("ab".to_string());
validate_input();
assert!(validation_errors.get().contains(&"Must be at least 3 characters".to_string()));
// Valid input - should have no errors
value.set("abc".to_string());
validate_input();
assert!(validation_errors.get().is_empty());
}
// ============================================================================
// TDD PATTERN 2: GREEN - Make tests pass with minimal implementation
// ============================================================================
#[derive(Clone, Debug)]
struct ValidationState {
pub is_valid: bool,
pub errors: Vec<String>,
}
impl ValidationState {
fn new() -> Self {
Self {
is_valid: true,
errors: Vec::new(),
}
}
fn add_error(&mut self, error: String) {
self.is_valid = false;
self.errors.push(error);
}
fn clear_errors(&mut self) {
self.is_valid = true;
self.errors.clear();
}
}
// ============================================================================
// TDD PATTERN 3: REFACTOR - Enhanced validation system tests
// ============================================================================
#[test]
fn test_enhanced_input_validation_system() {
use crate::validation::{InputValidator, ValidationContext, validation_builders};
// Test email validation with the new system
let email_validator = validation_builders::email_validator("email");
let result = email_validator.validate("invalid-email");
assert!(!result.is_valid);
assert!(result.errors.len() >= 1); // At least one error (email format)
assert!(result.errors.iter().any(|e| e.message.contains("valid email")));
// Test valid email
let valid_result = email_validator.validate("user@example.com");
assert!(valid_result.is_valid);
}
#[test]
fn test_password_validation_system() {
use crate::validation::validation_builders;
let password_validator = validation_builders::password_validator("password");
// Test weak password
let weak_result = password_validator.validate("weak");
assert!(!weak_result.is_valid);
assert!(weak_result.errors.len() >= 1); // At least one validation error
// Test strong password
let strong_result = password_validator.validate("StrongPass123");
assert!(strong_result.is_valid);
}
#[test]
fn test_validation_context_multiple_fields() {
use crate::validation::{ValidationContext, validation_builders};
let mut context = ValidationContext::new();
context.add_validator(validation_builders::email_validator("email"));
context.add_validator(validation_builders::username_validator("username"));
let mut values = std::collections::HashMap::new();
values.insert("email".to_string(), "invalid-email".to_string());
values.insert("username".to_string(), "ab".to_string()); // Too short
let result = context.validate_all(&values);
assert!(!result.is_valid);
assert!(result.errors.len() >= 1); // At least one validation error
// Test individual field validation
let email_error = context.get_field_error("email");
assert!(email_error.is_some());
let username_error = context.get_field_error("username");
assert!(username_error.is_some());
}
#[test]
fn test_custom_validation_rules() {
use crate::validation::InputValidator;
let validator = InputValidator::new("custom_field")
.required()
.custom(|value| value.starts_with("prefix_"));
// Test custom validation failure
let result = validator.validate("wrong_start");
assert!(!result.is_valid);
assert!(result.errors.len() >= 1);
// Test custom validation success
let valid_result = validator.validate("prefix_valid");
assert!(valid_result.is_valid);
}
#[test]
fn test_validation_error_prioritization() {
use crate::validation::InputValidator;
let validator = InputValidator::new("field")
.required()
.min_length(5)
.max_length(10);
// Empty field should show required error first
let empty_result = validator.validate("");
assert!(!empty_result.is_valid);
assert!(empty_result.errors[0].message.contains("required"));
// Short field should show min length error
let short_result = validator.validate("ab");
assert!(!short_result.is_valid);
assert!(short_result.errors[0].message.contains("at least"));
// Long field should show max length error
let long_result = validator.validate("very_long_input");
assert!(!long_result.is_valid);
assert!(long_result.errors[0].message.contains("no more than"));
}
#[test]
fn test_validation_performance() {
use crate::validation::InputValidator;
let validator = InputValidator::new("performance_test")
.required()
.min_length(3)
.max_length(100)
.pattern(r"^[a-zA-Z0-9]+$".to_string());
// Test that validation is fast even with multiple rules
let start = std::time::Instant::now();
for _ in 0..1000 {
let _ = validator.validate("test123");
}
let duration = start.elapsed();
// Should complete 1000 validations in reasonable time (< 1000ms)
assert!(duration.as_millis() < 1000, "Validation should be performant");
}
#[test]
fn test_input_base_css_classes() {
// Test that base INPUT_CLASS contains required styling and accessibility classes
assert!(INPUT_CLASS.contains("flex"));
assert!(INPUT_CLASS.contains("h-10"));
assert!(INPUT_CLASS.contains("w-full"));
assert!(INPUT_CLASS.contains("rounded-md"));
assert!(INPUT_CLASS.contains("border"));
assert!(INPUT_CLASS.contains("border-input"));
assert!(INPUT_CLASS.contains("bg-background"));
assert!(INPUT_CLASS.contains("focus-visible:outline-none"));
assert!(INPUT_CLASS.contains("focus-visible:ring-2"));
assert!(INPUT_CLASS.contains("disabled:cursor-not-allowed"));
assert!(INPUT_CLASS.contains("disabled:opacity-50"));
assert!(INPUT_CLASS.contains("placeholder:text-muted-foreground"));
}
#[test]
fn test_input_file_specific_classes() {
// Test file input specific styling
assert!(INPUT_CLASS.contains("file:border-0"));
assert!(INPUT_CLASS.contains("file:bg-transparent"));
assert!(INPUT_CLASS.contains("file:text-sm"));
assert!(INPUT_CLASS.contains("file:font-medium"));
}
#[test]
fn test_input_component_creation() {
// Test that Input component can be created with various props
// This is a conceptual test - in real implementation we'd need proper rendering environment
// Test default type
let default_type = "text".to_string();
assert_eq!(default_type, "text");
// Test various input types
let input_types = vec!["text", "password", "email", "number", "tel", "url"];
for input_type in input_types {
assert!(!input_type.is_empty());
}
}
#[test]
fn test_input_value_handling() {
// Test value prop handling
let test_value = "test value".to_string();
assert_eq!(test_value, "test value");
// Test empty value
let empty_value = String::new();
assert!(empty_value.is_empty());
// Test value updates
let mut value = RwSignal::new("initial".to_string());
assert_eq!(value.get(), "initial");
value.set("updated".to_string());
assert_eq!(value.get(), "updated");
}
#[test]
fn test_input_placeholder_handling() {
// Test placeholder functionality
let placeholder_text = "Enter text here...".to_string();
assert!(!placeholder_text.is_empty());
assert!(placeholder_text.contains("Enter"));
// Test empty placeholder
let empty_placeholder = String::new();
assert!(empty_placeholder.is_empty());
}
#[test]
fn test_input_disabled_state() {
// Test disabled signal functionality
let disabled_signal = RwSignal::new(false);
assert!(!disabled_signal.get());
disabled_signal.set(true);
assert!(disabled_signal.get());
// Test disabled state styling is included in base class
assert!(INPUT_CLASS.contains("disabled:cursor-not-allowed"));
assert!(INPUT_CLASS.contains("disabled:opacity-50"));
}
#[test]
fn test_input_change_callback() {
// Test change callback structure
let change_called = Arc::new(Mutex::new(false));
let change_value = Arc::new(Mutex::new(String::new()));
let change_called_clone = Arc::clone(&change_called);
let change_value_clone = Arc::clone(&change_value);
let callback = Callback::new(move |value: String| {
*change_called_clone.lock().unwrap() = true;
*change_value_clone.lock().unwrap() = value;
});
// Simulate callback execution
callback.run("test input".to_string());
assert!(*change_called.lock().unwrap());
assert_eq!(*change_value.lock().unwrap(), "test input");
}
#[test]
fn test_input_class_merging() {
// Test custom class handling
let base_class = INPUT_CLASS;
let custom_class = "my-custom-input-class";
let expected = format!("{} {}", base_class, custom_class);
assert!(expected.contains(base_class));
assert!(expected.contains(custom_class));
assert!(expected.len() > base_class.len());
}
#[test]
fn test_input_accessibility_features() {
// Test accessibility-related CSS classes
assert!(INPUT_CLASS.contains("focus-visible:outline-none"));
assert!(INPUT_CLASS.contains("focus-visible:ring-2"));
assert!(INPUT_CLASS.contains("focus-visible:ring-ring"));
assert!(INPUT_CLASS.contains("focus-visible:ring-offset-2"));
// Test that placeholder has proper contrast
assert!(INPUT_CLASS.contains("placeholder:text-muted-foreground"));
}
#[test]
fn test_input_styling_consistency() {
// Test that all required styling properties are present
let required_properties = vec![
"flex", "h-10", "w-full", "rounded-md", "border",
"bg-background", "px-3", "py-2", "text-sm",
"ring-offset-background"
];
for property in required_properties {
assert!(INPUT_CLASS.contains(property),
"INPUT_CLASS should contain '{}' property", property);
}
}
}