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        if 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
191    classes
192}
193
194/// Test a component's classes
195pub fn test_component_classes<F>(component_fn: F, expected_classes: &[&str]) -> TestResult
196where
197    F: Fn() -> String,
198{
199    let actual_classes = extract_classes(component_fn);
200    let expected_set: HashSet<String> = expected_classes.iter().map(|s| s.to_string()).collect();
201
202    let missing_classes: Vec<String> = expected_set.difference(&actual_classes).cloned().collect();
203    let extra_classes: Vec<String> = actual_classes.difference(&expected_set).cloned().collect();
204
205    if missing_classes.is_empty() && extra_classes.is_empty() {
206        TestResult::success("All expected classes found")
207    } else {
208        let mut message = String::new();
209        if !missing_classes.is_empty() {
210            message.push_str(&format!("Missing classes: {:?}", missing_classes));
211        }
212        if !extra_classes.is_empty() {
213            if !message.is_empty() {
214                message.push_str("; ");
215            }
216            message.push_str(&format!("Extra classes: {:?}", extra_classes));
217        }
218        TestResult::failure(message)
219    }
220}
221
222/// Test a component's HTML structure
223pub fn test_component_html<F>(component_fn: F, expected_html: &str) -> TestResult
224where
225    F: Fn() -> String,
226{
227    let actual_html = component_fn();
228
229    if actual_html.contains(expected_html) {
230        TestResult::success("Expected HTML found")
231    } else {
232        TestResult::failure(format!(
233            "Expected HTML not found. Expected: '{}', Got: '{}'",
234            expected_html, actual_html
235        ))
236    }
237}
238
239/// Test a component's custom properties
240pub fn test_component_custom_properties<F>(
241    component_fn: F,
242    expected_properties: &[(&str, &str)],
243) -> TestResult
244where
245    F: Fn() -> String,
246{
247    let html = component_fn();
248    let mut found_properties = std::collections::HashMap::new();
249
250    // Simple parsing for style attributes
251    if let Some(style_start) = html.find("style=\"") {
252        if let Some(style_end) = html[style_start + 7..].find("\"") {
253            let style_content = &html[style_start + 7..style_start + 7 + style_end];
254
255            for property in style_content.split(';') {
256                if let Some(colon_pos) = property.find(':') {
257                    let prop = property[..colon_pos].trim();
258                    let value = property[colon_pos + 1..].trim();
259                    found_properties.insert(prop.to_string(), value.to_string());
260                }
261            }
262        }
263    }
264
265    let mut missing_properties = Vec::new();
266    let mut incorrect_properties = Vec::new();
267
268    for (expected_prop, expected_value) in expected_properties {
269        if let Some(actual_value) = found_properties.get(*expected_prop) {
270            if actual_value != expected_value {
271                incorrect_properties.push(format!(
272                    "{}: expected '{}', got '{}'",
273                    expected_prop, expected_value, actual_value
274                ));
275            }
276        } else {
277            missing_properties.push(expected_prop.to_string());
278        }
279    }
280
281    if missing_properties.is_empty() && incorrect_properties.is_empty() {
282        TestResult::success("All expected custom properties found")
283    } else {
284        let mut message = String::new();
285        if !missing_properties.is_empty() {
286            message.push_str(&format!("Missing properties: {:?}", missing_properties));
287        }
288        if !incorrect_properties.is_empty() {
289            if !message.is_empty() {
290                message.push_str("; ");
291            }
292            message.push_str(&format!("Incorrect properties: {:?}", incorrect_properties));
293        }
294        TestResult::failure(message)
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    #[test]
303    fn test_test_app_creation() {
304        let config = TestConfig::default();
305        let app = TestApp::new(config);
306        assert!(app.components.is_empty());
307    }
308
309    #[test]
310    fn test_test_component_creation() {
311        let component = TestComponent::new("test", "<div>Test</div>");
312        assert_eq!(component.name, "test");
313        assert_eq!(component.html, "<div>Test</div>");
314        assert!(component.classes.is_empty());
315    }
316
317    #[test]
318    fn test_test_component_classes() {
319        let mut component = TestComponent::new("test", "<div>Test</div>");
320        component.add_class("bg-blue-500");
321        component.add_class("text-white");
322
323        assert!(component.has_class("bg-blue-500"));
324        assert!(component.has_class("text-white"));
325        assert!(!component.has_class("bg-red-500"));
326
327        let classes = component.get_classes();
328        assert_eq!(classes.len(), 2);
329        assert!(classes.contains(&"bg-blue-500".to_string()));
330        assert!(classes.contains(&"text-white".to_string()));
331    }
332
333    #[test]
334    fn test_extract_classes_from_html() {
335        let html = r#"<div class="bg-blue-500 text-white hover:bg-blue-600">Test</div>"#;
336        let classes = extract_classes_from_html(html);
337
338        assert!(classes.contains("bg-blue-500"));
339        assert!(classes.contains("text-white"));
340        assert!(classes.contains("hover:bg-blue-600"));
341    }
342
343    #[test]
344    fn test_test_component_classes_function() {
345        let component_fn = || r#"<div class="bg-blue-500 text-white">Test</div>"#.to_string();
346        let result = test_component_classes(component_fn, &["bg-blue-500", "text-white"]);
347
348        assert!(result.success);
349    }
350
351    #[test]
352    fn test_test_component_html_function() {
353        let component_fn = || r#"<div class="bg-blue-500">Test</div>"#.to_string();
354        let result = test_component_html(component_fn, "bg-blue-500");
355
356        assert!(result.success);
357    }
358
359    #[test]
360    fn test_test_component_custom_properties_function() {
361        let component_fn =
362            || r#"<div style="--primary-color: #3b82f6; --spacing: 1rem">Test</div>"#.to_string();
363        let result = test_component_custom_properties(
364            component_fn,
365            &[("--primary-color", "#3b82f6"), ("--spacing", "1rem")],
366        );
367
368        assert!(result.success);
369    }
370}