Files
leptos-shadcn-ui/packages/test-utils/src/dom_testing.rs
Peter Hanssens ec459d142c 🔧 Complete refactoring implementation for v0.9.0
- All 9 components fully refactored with modular architecture
- 45+ test modules created and organized
- File size compliance achieved (99% reduction)
- Enterprise-grade code quality implemented
- All compilation issues resolved

Ready for v0.9.0 release publication!
2025-09-22 13:06:22 +10:00

270 lines
9.2 KiB
Rust

//! DOM testing utilities for Leptos ShadCN UI components
//!
//! This module provides basic DOM rendering test capabilities to complement
//! the unit tests. It uses wasm-bindgen-test for browser-based testing.
use leptos::prelude::*;
use leptos::mount::mount_to;
use wasm_bindgen_test::*;
use web_sys::wasm_bindgen::JsCast;
wasm_bindgen_test_configure!(run_in_browser);
/// A test harness for rendering Leptos components in a test environment
pub struct ComponentTestHarness {
mount_point: String,
}
impl Default for ComponentTestHarness {
fn default() -> Self {
Self::new()
}
}
impl ComponentTestHarness {
/// Create a new test harness with a unique mount point
pub fn new() -> Self {
let mount_id = format!("test-mount-{}", uuid::Uuid::new_v4());
Self {
mount_point: mount_id,
}
}
/// Render a component and return the DOM element for testing
pub fn render<F>(&self, component: F) -> web_sys::HtmlElement
where
F: Fn() -> AnyView + 'static,
{
let document = web_sys::window().unwrap().document().unwrap();
// Create mount point
let mount_element = document.create_element("div").unwrap();
let mount_element = mount_element.dyn_into::<web_sys::HtmlElement>().unwrap();
mount_element.set_id(&self.mount_point);
document.body().unwrap().append_child(&mount_element).unwrap();
// Mount the component
let _dispose = mount_to(
mount_element.clone(),
component
);
// Store dispose function for cleanup (in real implementation)
// For now, return the mount element
mount_element
}
/// Helper to query for elements by CSS selector
pub fn query_selector(&self, selector: &str) -> Option<web_sys::Element> {
let document = web_sys::window().unwrap().document().unwrap();
let mount_element = document.get_element_by_id(&self.mount_point)?;
mount_element.query_selector(selector).unwrap_or(None)
}
/// Helper to get element text content
pub fn get_text_content(&self, selector: &str) -> Option<String> {
self.query_selector(selector)?.text_content()
}
/// Helper to check if element has specific class
pub fn has_class(&self, selector: &str, class_name: &str) -> bool {
if let Some(element) = self.query_selector(selector) {
element.class_list().contains(class_name)
} else {
false
}
}
/// Helper to get computed style
pub fn get_computed_style(&self, selector: &str, property: &str) -> Option<String> {
let element = self.query_selector(selector)?;
let window = web_sys::window().unwrap();
let computed_style = window.get_computed_style(&element).unwrap()?;
computed_style.get_property_value(property).unwrap_or_default().into()
}
/// Cleanup the test harness
pub fn cleanup(&self) {
if let Some(document) = web_sys::window().and_then(|w| w.document())
&& let Some(element) = document.get_element_by_id(&self.mount_point) {
element.remove();
}
}
}
impl Drop for ComponentTestHarness {
fn drop(&mut self) {
self.cleanup();
}
}
/// Test utilities for component accessibility
pub struct AccessibilityTester;
impl AccessibilityTester {
/// Check if element has proper ARIA attributes
pub fn check_aria_attributes(element: &web_sys::Element) -> Vec<String> {
let mut missing_attributes = Vec::new();
// Check for common ARIA attributes based on element type
let tag_name = element.tag_name().to_lowercase();
match tag_name.as_str() {
"button" => {
if !element.has_attribute("aria-label") && element.text_content().unwrap_or_default().is_empty() {
missing_attributes.push("aria-label or text content".to_string());
}
},
"input" => {
if !element.has_attribute("aria-label") && !element.has_attribute("aria-labelledby") {
missing_attributes.push("aria-label or aria-labelledby".to_string());
}
},
_ => {}
}
missing_attributes
}
/// Check color contrast (simplified)
pub fn check_color_contrast(element: &web_sys::Element) -> bool {
// Simplified contrast check - in real implementation would use proper algorithms
let window = web_sys::window().unwrap();
if let Ok(Some(computed_style)) = window.get_computed_style(element) {
let color = computed_style.get_property_value("color").unwrap_or_default();
let background = computed_style.get_property_value("background-color").unwrap_or_default();
// Basic check - ensure we have both color and background
!color.is_empty() && !background.is_empty()
} else {
false
}
}
/// Check keyboard navigation
pub fn is_keyboard_accessible(element: &web_sys::Element) -> bool {
// Check if element is focusable
element.has_attribute("tabindex") ||
matches!(element.tag_name().to_lowercase().as_str(), "button" | "input" | "select" | "textarea" | "a")
}
}
/// Performance testing utilities
pub struct PerformanceTester;
impl PerformanceTester {
/// Measure component render time
pub fn measure_render_time<F>(render_fn: F) -> f64
where
F: FnOnce(),
{
let performance = web_sys::window().unwrap().performance().unwrap();
let start = performance.now();
render_fn();
let end = performance.now();
end - start
}
/// Check bundle size impact (simplified)
pub fn estimate_bundle_impact(component_name: &str) -> usize {
// Simplified estimation - in real implementation would measure actual bundle sizes
match component_name {
"button" | "input" | "label" => 1024, // ~1KB
"card" | "dialog" => 2048, // ~2KB
"table" | "calendar" => 4096, // ~4KB
_ => 1500, // Default estimation
}
}
}
/// Macro to create DOM tests more easily
#[macro_export]
macro_rules! dom_test {
($test_name:ident, $component:expr, $test_body:block) => {
#[wasm_bindgen_test]
fn $test_name() {
let harness = ComponentTestHarness::new();
let _element = harness.render(|| $component);
$test_body
// Cleanup is handled by Drop trait
}
};
}
/// Example usage and integration tests
#[cfg(test)]
mod tests {
use super::*;
use leptos::prelude::*;
// Example DOM test for Button component
// Note: This would require the actual Button component to be imported
#[wasm_bindgen_test]
fn test_button_dom_rendering() {
let harness = ComponentTestHarness::new();
// This is a conceptual test - would need actual Button component
let _element = harness.render(|| {
view! {
<button class="btn-primary">{"Test Button"}</button>
}.into_any()
});
// Test that button rendered correctly
assert!(harness.query_selector("button").is_some());
assert_eq!(harness.get_text_content("button"), Some("Test Button".to_string()));
assert!(harness.has_class("button", "btn-primary"));
}
#[wasm_bindgen_test]
fn test_accessibility_checking() {
let document = web_sys::window().unwrap().document().unwrap();
let button = document.create_element("button").unwrap();
button.set_text_content(Some("Accessible Button"));
let missing_attrs = AccessibilityTester::check_aria_attributes(&button);
assert!(missing_attrs.is_empty(), "Button with text content should not need additional ARIA labels");
assert!(AccessibilityTester::is_keyboard_accessible(&button));
}
#[wasm_bindgen_test]
fn test_performance_measurement() {
let render_time = PerformanceTester::measure_render_time(|| {
// Simulate component rendering work
for _ in 0..1000 {
let _ = web_sys::window().unwrap().document().unwrap().create_element("div");
}
});
assert!(render_time > 0.0, "Should measure some render time");
assert!(render_time < 1000.0, "Render time should be reasonable (< 1 second)");
}
}
/// Integration with existing TDD framework
impl crate::TestResult {
/// Create a DOM test result
pub fn dom_test(passed: bool, component: &str, test_type: &str, details: Option<String>) -> Self {
let message = if passed {
format!("✅ DOM test passed: {} {}", component, test_type)
} else {
format!("❌ DOM test failed: {} {}", component, test_type)
};
let mut result = if passed {
Self::success(message)
} else {
Self::failure(message)
};
if let Some(details) = details {
result = result.with_detail("details", details);
}
result.with_detail("test_type", "dom")
.with_detail("component", component)
}
}