Files
leptos-shadcn-ui/docs/design/input.md
Peter Hanssens c3759fb019 feat: Complete Phase 2 Infrastructure Implementation
🏗️ MAJOR MILESTONE: Phase 2 Infrastructure Complete

This commit delivers a comprehensive, production-ready infrastructure system
for leptos-shadcn-ui with full automation, testing, and monitoring capabilities.

## 🎯 Infrastructure Components Delivered

### 1. WASM Browser Testing 
- Cross-browser WASM compatibility testing (Chrome, Firefox, Safari, Mobile)
- Performance monitoring with initialization time, memory usage, interaction latency
- Memory leak detection and pressure testing
- Automated error handling and recovery
- Bundle analysis and optimization recommendations
- Comprehensive reporting (HTML, JSON, Markdown)

### 2. E2E Test Integration 
- Enhanced Playwright configuration with CI/CD integration
- Multi-browser testing with automated execution
- Performance regression testing and monitoring
- Comprehensive reporting with artifact management
- Environment detection (CI vs local)
- GitHub Actions workflow with notifications

### 3. Performance Benchmarking 
- Automated regression testing with baseline comparison
- Real-time performance monitoring with configurable intervals
- Multi-channel alerting (console, file, webhook, email)
- Performance trend analysis and prediction
- CLI benchmarking tools and automated monitoring
- Baseline management and optimization recommendations

### 4. Accessibility Automation 
- WCAG compliance testing (A, AA, AAA levels)
- Comprehensive accessibility audit automation
- Screen reader support and keyboard navigation testing
- Color contrast and focus management validation
- Custom accessibility rules and violation detection
- Component-specific accessibility testing

## 🚀 Key Features

- **Production Ready**: All systems ready for immediate production use
- **CI/CD Integration**: Complete GitHub Actions workflow
- **Automated Monitoring**: Real-time performance and accessibility monitoring
- **Cross-Browser Support**: Chrome, Firefox, Safari, Mobile Chrome, Mobile Safari
- **Comprehensive Reporting**: Multiple output formats with detailed analytics
- **Error Recovery**: Graceful failure handling and recovery mechanisms

## 📁 Files Added/Modified

### New Infrastructure Files
- tests/e2e/wasm-browser-testing.spec.ts
- tests/e2e/wasm-performance-monitor.ts
- tests/e2e/wasm-test-config.ts
- tests/e2e/e2e-test-runner.ts
- tests/e2e/accessibility-automation.ts
- tests/e2e/accessibility-enhanced.spec.ts
- performance-audit/src/regression_testing.rs
- performance-audit/src/automated_monitoring.rs
- performance-audit/src/bin/performance-benchmark.rs
- scripts/run-wasm-tests.sh
- scripts/run-performance-benchmarks.sh
- scripts/run-accessibility-audit.sh
- .github/workflows/e2e-tests.yml
- playwright.config.ts

### Enhanced Configuration
- Enhanced Makefile with comprehensive infrastructure commands
- Enhanced global setup and teardown for E2E tests
- Performance audit system integration

### Documentation
- docs/infrastructure/PHASE2_INFRASTRUCTURE_GUIDE.md
- docs/infrastructure/INFRASTRUCTURE_SETUP_GUIDE.md
- docs/infrastructure/PHASE2_COMPLETION_SUMMARY.md
- docs/testing/WASM_TESTING_GUIDE.md

## 🎯 Usage

### Quick Start
```bash
# Run all infrastructure tests
make test

# Run WASM browser tests
make test-wasm

# Run E2E tests
make test-e2e-enhanced

# Run performance benchmarks
make benchmark

# Run accessibility audit
make accessibility-audit
```

### Advanced Usage
```bash
# Run tests on specific browsers
make test-wasm-browsers BROWSERS=chromium,firefox

# Run with specific WCAG level
make accessibility-audit-wcag LEVEL=AAA

# Run performance regression tests
make regression-test

# Start automated monitoring
make performance-monitor
```

## 📊 Performance Metrics

- **WASM Initialization**: <5s (Chrome) to <10s (Mobile Safari)
- **First Paint**: <3s (Chrome) to <5s (Mobile Safari)
- **Interaction Latency**: <100ms average
- **Memory Usage**: <50% increase during operations
- **WCAG Compliance**: AA level with AAA support

## 🎉 Impact

This infrastructure provides:
- **Reliable Component Development**: Comprehensive testing and validation
- **Performance Excellence**: Automated performance monitoring and optimization
- **Accessibility Compliance**: WCAG compliance validation and reporting
- **Production Deployment**: CI/CD integration with automated testing

## 🚀 Next Steps

Ready for Phase 3: Component Completion
- Complete remaining 41 components using established patterns
- Leverage infrastructure for comprehensive testing
- Ensure production-ready quality across all components

**Status**:  PHASE 2 COMPLETE - READY FOR PRODUCTION

Closes: Phase 2 Infrastructure Implementation
Related: #infrastructure #testing #automation #ci-cd
2025-09-20 12:31:11 +10:00

14 KiB

Input Component Design Specification

Overview & Purpose

The Input component is a foundational form element that handles text input with comprehensive validation, accessibility features, and integration with form libraries. It serves as the base for all text-based form interactions.

Component Type: Form/Input
Priority: P0 (Critical - essential for forms)
Dependencies: Label (optional), ValidationContext

API Specification

Props Interface

#[derive(Props, PartialEq)]
pub struct InputProps {
    /// Input type (text, email, password, etc.)
    #[prop(into, optional)]
    pub input_type: MaybeProp<InputType>,
    
    /// Current value
    #[prop(into, optional)]
    pub value: MaybeProp<String>,
    
    /// Placeholder text
    #[prop(into, optional)]
    pub placeholder: MaybeProp<String>,
    
    /// Disabled state
    #[prop(into, optional)]
    pub disabled: MaybeProp<bool>,
    
    /// Required field indicator
    #[prop(into, optional)]
    pub required: MaybeProp<bool>,
    
    /// Readonly state
    #[prop(into, optional)]
    pub readonly: MaybeProp<bool>,
    
    /// Input change handler
    #[prop(optional)]
    pub on_input: Option<Callback<String>>,
    
    /// Focus event handler
    #[prop(optional)]
    pub on_focus: Option<Callback<web_sys::FocusEvent>>,
    
    /// Blur event handler (validation trigger)
    #[prop(optional)]
    pub on_blur: Option<Callback<web_sys::FocusEvent>>,
    
    /// Validation rules
    #[prop(optional)]
    pub validator: Option<InputValidator>,
    
    /// Error state override
    #[prop(into, optional)]
    pub error: MaybeProp<Option<String>>,
    
    /// Success state override
    #[prop(into, optional)]
    pub success: MaybeProp<bool>,
    
    /// Custom CSS classes
    #[prop(into, optional)]
    pub class: MaybeProp<String>,
    
    /// HTML id attribute
    #[prop(into, optional)]
    pub id: MaybeProp<String>,
    
    /// HTML name attribute (form binding)
    #[prop(into, optional)]
    pub name: MaybeProp<String>,
    
    /// ARIA label for accessibility
    #[prop(into, optional)]
    pub aria_label: MaybeProp<String>,
    
    /// ARIA described by (error messages)
    #[prop(into, optional)]
    pub aria_describedby: MaybeProp<String>,
}

Enums and Types

#[derive(Debug, Clone, PartialEq)]
pub enum InputType {
    Text,
    Email,
    Password,
    Tel,
    Url,
    Search,
    Number,
}

impl Default for InputType {
    fn default() -> Self {
        Self::Text
    }
}

#[derive(Debug, Clone, PartialEq)]
pub enum InputSize {
    Small,    // Compact form inputs
    Default,  // Standard size
    Large,    // Prominent inputs
}

#[derive(Debug, Clone)]
pub struct InputValidator {
    pub rules: Vec<ValidationRule>,
    pub on_validation: Option<Callback<ValidationResult>>,
}

#[derive(Debug, Clone, PartialEq)]
pub enum ValidationRule {
    Required,
    MinLength(usize),
    MaxLength(usize),
    Email,
    Pattern(String),
    Custom(fn(&str) -> Result<(), String>),
}

#[derive(Debug, Clone, PartialEq)]
pub struct ValidationResult {
    pub is_valid: bool,
    pub errors: Vec<String>,
    pub field_name: String,
}

Behavioral Requirements

Core Behaviors

  1. Text Input: Accepts and displays user text input
  2. Real-time Validation: Validates on input/blur based on rules
  3. State Management: Tracks focus, error, success states
  4. Form Integration: Works with form libraries and native forms
  5. Accessibility: Full screen reader and keyboard support

State Transitions

[Empty] --input--> [Filled] --validate--> [Valid/Invalid]
[Any] --focus--> [Focused] --blur--> [Unfocused + Validated]
[Any] --disabled--> [Disabled] --enabled--> [Previous State]

Validation Timing

  • On Input: Real-time for immediate feedback (debounced 300ms)
  • On Blur: Comprehensive validation when field loses focus
  • On Submit: Final validation before form submission
  • On Mount: Initial validation if value provided

Event Handling

// Input event with debouncing
let handle_input = move |event: web_sys::Event| {
    let input = event_target_value(&event);
    value_signal.set(input.clone());
    
    // Debounced validation
    debounced_validate.call(input);
    
    if let Some(on_input) = on_input {
        on_input.call(input);
    }
};

// Blur event for validation
let handle_blur = move |event: web_sys::FocusEvent| {
    set_focused(false);
    validate_field();
    
    if let Some(on_blur) = on_blur {
        on_blur.call(event);
    }
};

Accessibility Requirements

WCAG 2.1 AA Compliance

  • Labels: Associated with <label> element or aria-label
  • Error Messages: Linked via aria-describedby
  • Required Fields: Indicated with required attribute and aria-required
  • Invalid State: Marked with aria-invalid="true"
  • Focus Management: Clear focus indicators and logical tab order

Screen Reader Support

<div>
  <label for="email-input" class="sr-only">Email Address</label>
  <input
    type="email"
    id="email-input"
    name="email"
    placeholder="Enter email address"
    required
    aria-required="true"
    aria-invalid="false"
    aria-describedby="email-error email-help"
  />
  <div id="email-help" class="sr-only">
    We'll never share your email address
  </div>
  <div id="email-error" role="alert" aria-live="polite">
    <!-- Error messages inserted here -->
  </div>
</div>

Keyboard Navigation

  • Tab: Focuses the input field
  • Enter: Submits parent form (if applicable)
  • Escape: Clears field (optional behavior)

Styling & Theming

Base Styles

const BASE_CLASSES: &str = "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50";

State-based Styles

fn input_classes(
    has_error: bool,
    has_success: bool, 
    is_focused: bool,
    disabled: bool,
) -> String {
    let mut classes = vec![BASE_CLASSES];
    
    match (has_error, has_success, disabled) {
        (true, _, false) => classes.push("border-destructive focus-visible:ring-destructive"),
        (false, true, false) => classes.push("border-success focus-visible:ring-success"),
        (_, _, true) => classes.push("border-muted bg-muted"),
        _ => {}
    }
    
    if is_focused && !disabled {
        classes.push("ring-2 ring-ring ring-offset-2");
    }
    
    classes.join(" ")
}

Size Variants

fn size_classes(size: InputSize) -> &'static str {
    match size {
        InputSize::Small => "h-8 px-2 py-1 text-xs",
        InputSize::Default => "h-10 px-3 py-2 text-sm", 
        InputSize::Large => "h-12 px-4 py-3 text-base",
    }
}

Testing Strategy

Unit Tests

#[cfg(test)]
mod tests {
    use super::*;
    use leptos_testing::*;
    
    #[wasm_bindgen_test]
    fn renders_basic_input() {
        let result = render_component(|| {
            view! { <Input placeholder="Enter text" /> }
        });
        
        assert_element_exists(&result, "input");
        assert_element_attribute(&result, "input", "placeholder", "Enter text");
        assert_element_has_class(&result, "input", "border-input");
    }
    
    #[wasm_bindgen_test]
    fn handles_input_events() {
        let value = create_rw_signal(String::new());
        
        let result = render_component(|| {
            view! {
                <Input 
                    value=value.get()
                    on_input=move |v| value.set(v)
                />
            }
        });
        
        type_in_element(&result, "input", "Hello World");
        assert_eq!(value.get(), "Hello World");
    }
    
    #[wasm_bindgen_test]
    fn validates_required_field() {
        let validator = InputValidator::new()
            .required()
            .build();
            
        let result = render_component(|| {
            view! {
                <Input 
                    validator=validator
                    value=""
                />
            }
        });
        
        blur_element(&result, "input");
        
        assert_element_has_attribute(&result, "input", "aria-invalid", "true");
        assert_element_exists(&result, "[role='alert']");
    }
    
    #[wasm_bindgen_test]
    fn validates_email_format() {
        let validator = InputValidator::new()
            .email()
            .build();
            
        let result = render_component(|| {
            view! {
                <Input 
                    input_type=InputType::Email
                    validator=validator
                    value="invalid-email"
                />
            }
        });
        
        blur_element(&result, "input");
        
        assert_element_has_attribute(&result, "input", "aria-invalid", "true");
        assert_element_text_contains(&result, "[role='alert']", "valid email");
    }
}

Integration Tests

#[wasm_bindgen_test]
async fn integrates_with_form() {
    let form_submitted = create_rw_signal(false);
    
    let result = render_component(|| {
        view! {
            <form on_submit=move |e| {
                e.prevent_default();
                form_submitted.set(true);
            }>
                <Input name="username" required=true />
                <button type="submit">"Submit"</button>
            </form>
        }
    });
    
    type_in_element(&result, "input", "testuser");
    click_element(&result, "button");
    
    assert!(form_submitted.get());
}

Accessibility Tests

#[wasm_bindgen_test]
async fn meets_accessibility_standards() {
    let result = render_component(|| {
        view! {
            <div>
                <label for="test-input">"Test Input"</label>
                <Input id="test-input" required=true />
            </div>
        }
    });
    
    // Run axe-core checks
    assert_accessible(&result).await;
    
    // Test specific a11y requirements
    assert_element_has_attribute(&result, "input", "aria-required", "true");
    assert_elements_associated(&result, "label", "input");
}

Performance Tests

#[wasm_bindgen_test]
fn handles_rapid_input_efficiently() {
    let result = render_component(|| {
        view! { <Input validator=complex_validator /> }
    });
    
    let start = performance::now();
    
    // Simulate rapid typing
    for i in 0..100 {
        type_in_element(&result, "input", &format!("text{}", i));
    }
    
    let duration = performance::now() - start;
    assert!(duration < 1000.0, "Should handle rapid input in <1s");
}

Implementation Notes

Debounced Validation

fn create_debounced_validator(
    validator: Option<InputValidator>,
    delay_ms: u32,
) -> impl Fn(String) {
    let timeout_handle = create_rw_signal(None::<i32>);
    
    move |value: String| {
        // Clear existing timeout
        if let Some(handle) = timeout_handle.get() {
            clear_timeout(handle);
        }
        
        // Set new timeout
        let new_handle = set_timeout(
            move || validate_value(value.clone(), &validator),
            delay_ms,
        );
        timeout_handle.set(Some(new_handle));
    }
}

Form Integration

impl Input {
    // Register with form context on mount
    fn register_with_form(&self) {
        if let Some(form_context) = use_context::<FormContext>() {
            form_context.register_field(FormField {
                name: self.name.clone(),
                value: self.value_signal,
                validator: self.validator.clone(),
                errors: self.errors_signal,
            });
        }
    }
}

Memory Management

  • Use create_memo for computed validation state
  • Clean up event listeners on unmount
  • Debounce validation to prevent excessive calls

Examples & Usage

Basic Text Input

view! {
    <Input 
        placeholder="Enter your name"
        on_input=move |value| console::log!("Input: {}", value)
    />
}

Email Input with Validation

let email_validator = InputValidator::new()
    .required()
    .email()
    .build();

view! {
    <Input 
        input_type=InputType::Email
        placeholder="your@email.com"
        validator=email_validator
        required=true
    />
}

Password Input with Strength Validation

let password_validator = InputValidator::new()
    .required()
    .min_length(8)
    .pattern(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])")
    .build();

view! {
    <Input 
        input_type=InputType::Password
        placeholder="Strong password"
        validator=password_validator
    />
}

Controlled Input with External State

fn ControlledInput() -> impl IntoView {
    let value = create_rw_signal(String::new());
    let char_count = create_memo(move |_| value.get().len());
    
    view! {
        <div>
            <Input 
                value=value.get()
                on_input=move |v| value.set(v)
                placeholder="Type something..."
            />
            <div class="text-sm text-muted-foreground">
                {char_count} " characters"
            </div>
        </div>
    }
}

Form Integration Example

view! {
    <form class="space-y-4">
        <div>
            <label for="username">"Username"</label>
            <Input 
                id="username"
                name="username" 
                required=true
                validator=username_validator
            />
        </div>
        
        <div>
            <label for="email">"Email"</label>
            <Input 
                id="email"
                name="email"
                input_type=InputType::Email
                required=true
                validator=email_validator
            />
        </div>
        
        <button type="submit">"Submit"</button>
    </form>
}