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 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
194pub 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
222pub 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
239pub 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 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}