tailwind_rs_testing/
component_testing.rs

1//! Component testing utilities for tailwind-rs
2
3use crate::{TestConfig, TestResult};
4use std::collections::HashSet;
5
6/// A test application for component testing
7#[derive(Debug, Clone)]
8pub struct TestApp {
9    pub config: TestConfig,
10    pub components: Vec<TestComponent>,
11}
12
13/// A test component with metadata
14#[derive(Debug, Clone)]
15pub struct TestComponent {
16    pub name: String,
17    pub html: String,
18    pub classes: HashSet<String>,
19    pub custom_properties: std::collections::HashMap<String, String>,
20}
21
22impl TestApp {
23    /// Create a new test app
24    pub fn new(config: TestConfig) -> Self {
25        Self {
26            config,
27            components: Vec::new(),
28        }
29    }
30
31    /// Add a component to the test app
32    pub fn add_component(&mut self, component: TestComponent) {
33        self.components.push(component);
34    }
35
36    /// Get a component by name
37    pub fn get_component(&self, name: &str) -> Option<&TestComponent> {
38        self.components.iter().find(|c| c.name == name)
39    }
40
41    /// Get all components
42    pub fn get_components(&self) -> &[TestComponent] {
43        &self.components
44    }
45
46    /// Set the theme for the test app
47    pub fn set_theme(&mut self, theme: String) {
48        self.config.theme = Some(theme);
49    }
50
51    /// Set the breakpoint for the test app
52    pub fn set_breakpoint(&mut self, breakpoint: String) {
53        self.config.breakpoint = Some(breakpoint);
54    }
55
56    /// Enable dark mode for the test app
57    pub fn enable_dark_mode(&mut self) {
58        self.config.dark_mode = true;
59    }
60
61    /// Disable dark mode for the test app
62    pub fn disable_dark_mode(&mut self) {
63        self.config.dark_mode = false;
64    }
65
66    /// Add custom CSS to the test app
67    pub fn add_custom_css(&mut self, css: String) {
68        self.config.custom_css.push(css);
69    }
70}
71
72impl TestComponent {
73    /// Create a new test component
74    pub fn new(name: impl Into<String>, html: impl Into<String>) -> Self {
75        Self {
76            name: name.into(),
77            html: html.into(),
78            classes: HashSet::new(),
79            custom_properties: std::collections::HashMap::new(),
80        }
81    }
82
83    /// Add a class to the component
84    pub fn add_class(&mut self, class: impl Into<String>) {
85        self.classes.insert(class.into());
86    }
87
88    /// Add multiple classes to the component
89    pub fn add_classes(&mut self, classes: impl IntoIterator<Item = String>) {
90        for class in classes {
91            self.classes.insert(class);
92        }
93    }
94
95    /// Add a custom property to the component
96    pub fn add_custom_property(&mut self, property: impl Into<String>, value: impl Into<String>) {
97        self.custom_properties.insert(property.into(), value.into());
98    }
99
100    /// Check if the component has a specific class
101    pub fn has_class(&self, class: &str) -> bool {
102        self.classes.contains(class)
103    }
104
105    /// Get all classes as a vector
106    pub fn get_classes(&self) -> Vec<String> {
107        self.classes.iter().cloned().collect()
108    }
109
110    /// Get all classes as a string
111    pub fn get_classes_string(&self) -> String {
112        let mut sorted_classes: Vec<String> = self.classes.iter().cloned().collect();
113        sorted_classes.sort();
114        sorted_classes.join(" ")
115    }
116
117    /// Get a custom property value
118    pub fn get_custom_property(&self, property: &str) -> Option<&String> {
119        self.custom_properties.get(property)
120    }
121
122    /// Get all custom properties
123    pub fn get_custom_properties(&self) -> &std::collections::HashMap<String, String> {
124        &self.custom_properties
125    }
126}
127
128/// Create a test app with default configuration
129pub fn create_test_app<F>(component_fn: F) -> TestApp
130where
131    F: Fn() -> String,
132{
133    let config = TestConfig::default();
134    let mut app = TestApp::new(config);
135
136    let html = component_fn();
137    let component = TestComponent::new("test_component", html);
138    app.add_component(component);
139
140    app
141}
142
143/// Create a test app with custom configuration
144pub fn create_test_app_with_config<F>(component_fn: F, config: TestConfig) -> TestApp
145where
146    F: Fn() -> String,
147{
148    let mut app = TestApp::new(config);
149
150    let html = component_fn();
151    let component = TestComponent::new("test_component", html);
152    app.add_component(component);
153
154    app
155}
156
157/// Render a component to HTML string
158pub fn render_to_string<F>(component_fn: F) -> String
159where
160    F: Fn() -> String,
161{
162    component_fn()
163}
164
165/// Extract CSS classes from a component
166pub fn extract_classes<F>(component_fn: F) -> HashSet<String>
167where
168    F: Fn() -> String,
169{
170    let html = component_fn();
171    extract_classes_from_html(&html)
172}
173
174/// Extract CSS classes from HTML string
175pub fn extract_classes_from_html(html: &str) -> HashSet<String> {
176    let mut classes = HashSet::new();
177
178    // Simple parsing for class attributes
179    if let Some(class_start) = html.find("class=\"")
180        && let Some(class_end) = html[class_start + 7..].find("\"") {
181        let class_content = &html[class_start + 7..class_start + 7 + class_end];
182
183        for class in class_content.split_whitespace() {
184            if !class.is_empty() {
185                classes.insert(class.to_string());
186            }
187        }
188    }
189
190    classes
191}
192
193/// Test a component's classes
194pub fn test_component_classes<F>(component_fn: F, expected_classes: &[&str]) -> TestResult
195where
196    F: Fn() -> String,
197{
198    let actual_classes = extract_classes(component_fn);
199    let expected_set: HashSet<String> = expected_classes.iter().map(|s| s.to_string()).collect();
200
201    let missing_classes: Vec<String> = expected_set.difference(&actual_classes).cloned().collect();
202    let extra_classes: Vec<String> = actual_classes.difference(&expected_set).cloned().collect();
203
204    if missing_classes.is_empty() && extra_classes.is_empty() {
205        TestResult::success("All expected classes found")
206    } else {
207        let mut message = String::new();
208        if !missing_classes.is_empty() {
209            message.push_str(&format!("Missing classes: {:?}", missing_classes));
210        }
211        if !extra_classes.is_empty() {
212            if !message.is_empty() {
213                message.push_str("; ");
214            }
215            message.push_str(&format!("Extra classes: {:?}", extra_classes));
216        }
217        TestResult::failure(message)
218    }
219}
220
221/// Test a component's HTML structure
222pub fn test_component_html<F>(component_fn: F, expected_html: &str) -> TestResult
223where
224    F: Fn() -> String,
225{
226    let actual_html = component_fn();
227
228    if actual_html.contains(expected_html) {
229        TestResult::success("Expected HTML found")
230    } else {
231        TestResult::failure(format!(
232            "Expected HTML not found. Expected: '{}', Got: '{}'",
233            expected_html, actual_html
234        ))
235    }
236}
237
238/// Test a component's custom properties
239pub fn test_component_custom_properties<F>(
240    component_fn: F,
241    expected_properties: &[(&str, &str)],
242) -> TestResult
243where
244    F: Fn() -> String,
245{
246    let html = component_fn();
247    let mut found_properties = std::collections::HashMap::new();
248
249    // Simple parsing for style attributes
250    if let Some(style_start) = html.find("style=\"")
251        && let Some(style_end) = html[style_start + 7..].find("\"") {
252        let style_content = &html[style_start + 7..style_start + 7 + style_end];
253
254        for property in style_content.split(';') {
255            if let Some(colon_pos) = property.find(':') {
256                let prop = property[..colon_pos].trim();
257                let value = property[colon_pos + 1..].trim();
258                found_properties.insert(prop.to_string(), value.to_string());
259            }
260        }
261    }
262
263    let mut missing_properties = Vec::new();
264    let mut incorrect_properties = Vec::new();
265
266    for (expected_prop, expected_value) in expected_properties {
267        if let Some(actual_value) = found_properties.get(*expected_prop) {
268            if actual_value != expected_value {
269                incorrect_properties.push(format!(
270                    "{}: expected '{}', got '{}'",
271                    expected_prop, expected_value, actual_value
272                ));
273            }
274        } else {
275            missing_properties.push(expected_prop.to_string());
276        }
277    }
278
279    if missing_properties.is_empty() && incorrect_properties.is_empty() {
280        TestResult::success("All expected custom properties found")
281    } else {
282        let mut message = String::new();
283        if !missing_properties.is_empty() {
284            message.push_str(&format!("Missing properties: {:?}", missing_properties));
285        }
286        if !incorrect_properties.is_empty() {
287            if !message.is_empty() {
288                message.push_str("; ");
289            }
290            message.push_str(&format!("Incorrect properties: {:?}", incorrect_properties));
291        }
292        TestResult::failure(message)
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn test_test_app_creation() {
302        let config = TestConfig::default();
303        let app = TestApp::new(config);
304        assert!(app.components.is_empty());
305    }
306
307    #[test]
308    fn test_test_component_creation() {
309        let component = TestComponent::new("test", "<div>Test</div>");
310        assert_eq!(component.name, "test");
311        assert_eq!(component.html, "<div>Test</div>");
312        assert!(component.classes.is_empty());
313    }
314
315    #[test]
316    fn test_test_component_classes() {
317        let mut component = TestComponent::new("test", "<div>Test</div>");
318        component.add_class("bg-blue-500");
319        component.add_class("text-white");
320
321        assert!(component.has_class("bg-blue-500"));
322        assert!(component.has_class("text-white"));
323        assert!(!component.has_class("bg-red-500"));
324
325        let classes = component.get_classes();
326        assert_eq!(classes.len(), 2);
327        assert!(classes.contains(&"bg-blue-500".to_string()));
328        assert!(classes.contains(&"text-white".to_string()));
329    }
330
331    #[test]
332    fn test_extract_classes_from_html() {
333        let html = r#"<div class="bg-blue-500 text-white hover:bg-blue-600">Test</div>"#;
334        let classes = extract_classes_from_html(html);
335
336        assert!(classes.contains("bg-blue-500"));
337        assert!(classes.contains("text-white"));
338        assert!(classes.contains("hover:bg-blue-600"));
339    }
340
341    #[test]
342    fn test_test_component_classes_function() {
343        let component_fn = || r#"<div class="bg-blue-500 text-white">Test</div>"#.to_string();
344        let result = test_component_classes(component_fn, &["bg-blue-500", "text-white"]);
345
346        assert!(result.success);
347    }
348
349    #[test]
350    fn test_test_component_html_function() {
351        let component_fn = || r#"<div class="bg-blue-500">Test</div>"#.to_string();
352        let result = test_component_html(component_fn, "bg-blue-500");
353
354        assert!(result.success);
355    }
356
357    #[test]
358    fn test_test_component_custom_properties_function() {
359        let component_fn =
360            || r#"<div style="--primary-color: #3b82f6; --spacing: 1rem">Test</div>"#.to_string();
361        let result = test_component_custom_properties(
362            component_fn,
363            &[("--primary-color", "#3b82f6"), ("--spacing", "1rem")],
364        );
365
366        assert!(result.success);
367    }
368}