tailwind_rs_testing/
component_testing.rs1use crate::{TestConfig, TestResult};
4use std::collections::HashSet;
5
6#[derive(Debug, Clone)]
8pub struct TestApp {
9 pub config: TestConfig,
10 pub components: Vec<TestComponent>,
11}
12
13#[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 pub fn new(config: TestConfig) -> Self {
25 Self {
26 config,
27 components: Vec::new(),
28 }
29 }
30
31 pub fn add_component(&mut self, component: TestComponent) {
33 self.components.push(component);
34 }
35
36 pub fn get_component(&self, name: &str) -> Option<&TestComponent> {
38 self.components.iter().find(|c| c.name == name)
39 }
40
41 pub fn get_components(&self) -> &[TestComponent] {
43 &self.components
44 }
45
46 pub fn set_theme(&mut self, theme: String) {
48 self.config.theme = Some(theme);
49 }
50
51 pub fn set_breakpoint(&mut self, breakpoint: String) {
53 self.config.breakpoint = Some(breakpoint);
54 }
55
56 pub fn enable_dark_mode(&mut self) {
58 self.config.dark_mode = true;
59 }
60
61 pub fn disable_dark_mode(&mut self) {
63 self.config.dark_mode = false;
64 }
65
66 pub fn add_custom_css(&mut self, css: String) {
68 self.config.custom_css.push(css);
69 }
70}
71
72impl TestComponent {
73 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 pub fn add_class(&mut self, class: impl Into<String>) {
85 self.classes.insert(class.into());
86 }
87
88 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 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 pub fn has_class(&self, class: &str) -> bool {
102 self.classes.contains(class)
103 }
104
105 pub fn get_classes(&self) -> Vec<String> {
107 self.classes.iter().cloned().collect()
108 }
109
110 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 pub fn get_custom_property(&self, property: &str) -> Option<&String> {
119 self.custom_properties.get(property)
120 }
121
122 pub fn get_custom_properties(&self) -> &std::collections::HashMap<String, String> {
124 &self.custom_properties
125 }
126}
127
128pub 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
143pub 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
157pub fn render_to_string<F>(component_fn: F) -> String
159where
160 F: Fn() -> String,
161{
162 component_fn()
163}
164
165pub 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
174pub fn extract_classes_from_html(html: &str) -> HashSet<String> {
176 let mut classes = HashSet::new();
177
178 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
193pub 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
221pub 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
238pub 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 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}