tailwind_rs_testing/
class_testing.rs

1//! Class testing utilities for tailwind-rs
2
3use std::collections::HashSet;
4
5/// Result of a class test
6#[derive(Debug, Clone)]
7pub struct ClassTestResult {
8    pub success: bool,
9    pub message: String,
10    pub expected_classes: HashSet<String>,
11    pub actual_classes: HashSet<String>,
12    pub missing_classes: HashSet<String>,
13    pub extra_classes: HashSet<String>,
14}
15
16impl ClassTestResult {
17    /// Create a successful test result
18    pub fn success(
19        message: impl Into<String>,
20        expected: HashSet<String>,
21        actual: HashSet<String>,
22    ) -> Self {
23        Self {
24            success: true,
25            message: message.into(),
26            expected_classes: expected,
27            actual_classes: actual,
28            missing_classes: HashSet::new(),
29            extra_classes: HashSet::new(),
30        }
31    }
32
33    /// Create a failed test result
34    pub fn failure(
35        message: impl Into<String>,
36        expected: HashSet<String>,
37        actual: HashSet<String>,
38        missing: HashSet<String>,
39        extra: HashSet<String>,
40    ) -> Self {
41        Self {
42            success: false,
43            message: message.into(),
44            expected_classes: expected,
45            actual_classes: actual,
46            missing_classes: missing,
47            extra_classes: extra,
48        }
49    }
50}
51
52/// Test CSS classes for correctness
53pub fn test_classes(
54    actual_classes: &HashSet<String>,
55    expected_classes: &HashSet<String>,
56) -> ClassTestResult {
57    let missing_classes: HashSet<String> = expected_classes
58        .difference(actual_classes)
59        .cloned()
60        .collect();
61    let extra_classes: HashSet<String> = actual_classes
62        .difference(expected_classes)
63        .cloned()
64        .collect();
65
66    if missing_classes.is_empty() && extra_classes.is_empty() {
67        ClassTestResult::success(
68            "All expected classes found",
69            expected_classes.clone(),
70            actual_classes.clone(),
71        )
72    } else {
73        let mut message = String::new();
74        if !missing_classes.is_empty() {
75            message.push_str(&format!("Missing classes: {:?}", missing_classes));
76        }
77        if !extra_classes.is_empty() {
78            if !message.is_empty() {
79                message.push_str("; ");
80            }
81            message.push_str(&format!("Extra classes: {:?}", extra_classes));
82        }
83
84        ClassTestResult::failure(
85            message,
86            expected_classes.clone(),
87            actual_classes.clone(),
88            missing_classes,
89            extra_classes,
90        )
91    }
92}
93
94/// Test that classes contain all expected classes
95pub fn test_classes_contain(
96    actual_classes: &HashSet<String>,
97    expected_classes: &HashSet<String>,
98) -> ClassTestResult {
99    let missing_classes: HashSet<String> = expected_classes
100        .difference(actual_classes)
101        .cloned()
102        .collect();
103
104    if missing_classes.is_empty() {
105        ClassTestResult::success(
106            "All expected classes found",
107            expected_classes.clone(),
108            actual_classes.clone(),
109        )
110    } else {
111        ClassTestResult::failure(
112            format!("Missing classes: {:?}", missing_classes),
113            expected_classes.clone(),
114            actual_classes.clone(),
115            missing_classes,
116            HashSet::new(),
117        )
118    }
119}
120
121/// Test that classes don't contain any unexpected classes
122pub fn test_classes_not_contain(
123    actual_classes: &HashSet<String>,
124    unexpected_classes: &HashSet<String>,
125) -> ClassTestResult {
126    let extra_classes: HashSet<String> = actual_classes
127        .intersection(unexpected_classes)
128        .cloned()
129        .collect();
130
131    if extra_classes.is_empty() {
132        ClassTestResult::success(
133            "No unexpected classes found",
134            HashSet::new(),
135            actual_classes.clone(),
136        )
137    } else {
138        ClassTestResult::failure(
139            format!("Unexpected classes found: {:?}", extra_classes),
140            HashSet::new(),
141            actual_classes.clone(),
142            HashSet::new(),
143            extra_classes,
144        )
145    }
146}
147
148/// Test that classes match exactly
149pub fn test_classes_exact(
150    actual_classes: &HashSet<String>,
151    expected_classes: &HashSet<String>,
152) -> ClassTestResult {
153    test_classes(actual_classes, expected_classes)
154}
155
156/// Test that classes contain at least one of the expected classes
157pub fn test_classes_any(
158    actual_classes: &HashSet<String>,
159    expected_classes: &HashSet<String>,
160) -> ClassTestResult {
161    let found_classes: HashSet<String> = actual_classes
162        .intersection(expected_classes)
163        .cloned()
164        .collect();
165
166    if !found_classes.is_empty() {
167        ClassTestResult::success(
168            format!("Found expected classes: {:?}", found_classes),
169            expected_classes.clone(),
170            actual_classes.clone(),
171        )
172    } else {
173        ClassTestResult::failure(
174            format!(
175                "No expected classes found. Expected any of: {:?}",
176                expected_classes
177            ),
178            expected_classes.clone(),
179            actual_classes.clone(),
180            expected_classes.clone(),
181            HashSet::new(),
182        )
183    }
184}
185
186/// Test that classes contain all of the expected classes
187pub fn test_classes_all(
188    actual_classes: &HashSet<String>,
189    expected_classes: &HashSet<String>,
190) -> ClassTestResult {
191    test_classes_contain(actual_classes, expected_classes)
192}
193
194/// Test that classes contain none of the unexpected classes
195pub fn test_classes_none(
196    actual_classes: &HashSet<String>,
197    unexpected_classes: &HashSet<String>,
198) -> ClassTestResult {
199    test_classes_not_contain(actual_classes, unexpected_classes)
200}
201
202/// Assert that classes contain all expected classes
203pub fn assert_classes_contain(
204    actual_classes: &HashSet<String>,
205    expected_classes: &HashSet<String>,
206) {
207    let result = test_classes_contain(actual_classes, expected_classes);
208    if !result.success {
209        panic!("Class assertion failed: {}", result.message);
210    }
211}
212
213/// Assert that classes don't contain any unexpected classes
214pub fn assert_classes_not_contain(
215    actual_classes: &HashSet<String>,
216    unexpected_classes: &HashSet<String>,
217) {
218    let result = test_classes_not_contain(actual_classes, unexpected_classes);
219    if !result.success {
220        panic!("Class assertion failed: {}", result.message);
221    }
222}
223
224/// Assert that classes match exactly
225pub fn assert_classes_exact(actual_classes: &HashSet<String>, expected_classes: &HashSet<String>) {
226    let result = test_classes_exact(actual_classes, expected_classes);
227    if !result.success {
228        panic!("Class assertion failed: {}", result.message);
229    }
230}
231
232/// Assert that classes contain at least one of the expected classes
233pub fn assert_classes_any(actual_classes: &HashSet<String>, expected_classes: &HashSet<String>) {
234    let result = test_classes_any(actual_classes, expected_classes);
235    if !result.success {
236        panic!("Class assertion failed: {}", result.message);
237    }
238}
239
240/// Assert that classes contain all of the expected classes
241pub fn assert_classes_all(actual_classes: &HashSet<String>, expected_classes: &HashSet<String>) {
242    let result = test_classes_all(actual_classes, expected_classes);
243    if !result.success {
244        panic!("Class assertion failed: {}", result.message);
245    }
246}
247
248/// Assert that classes contain none of the unexpected classes
249pub fn assert_classes_none(actual_classes: &HashSet<String>, unexpected_classes: &HashSet<String>) {
250    let result = test_classes_none(actual_classes, unexpected_classes);
251    if !result.success {
252        panic!("Class assertion failed: {}", result.message);
253    }
254}
255
256/// Test responsive classes
257pub fn test_responsive_classes(
258    actual_classes: &HashSet<String>,
259    expected_responsive: &[(&str, &str)],
260) -> ClassTestResult {
261    let mut missing_classes = HashSet::new();
262    let mut found_classes = HashSet::new();
263
264    for (breakpoint, class) in expected_responsive {
265        let responsive_class = format!("{}:{}", breakpoint, class);
266        if actual_classes.contains(&responsive_class) {
267            found_classes.insert(responsive_class);
268        } else {
269            missing_classes.insert(responsive_class);
270        }
271    }
272
273    if missing_classes.is_empty() {
274        ClassTestResult::success(
275            "All expected responsive classes found",
276            expected_responsive
277                .iter()
278                .map(|(bp, cls)| format!("{}:{}", bp, cls))
279                .collect(),
280            actual_classes.clone(),
281        )
282    } else {
283        ClassTestResult::failure(
284            format!("Missing responsive classes: {:?}", missing_classes),
285            expected_responsive
286                .iter()
287                .map(|(bp, cls)| format!("{}:{}", bp, cls))
288                .collect(),
289            actual_classes.clone(),
290            missing_classes,
291            HashSet::new(),
292        )
293    }
294}
295
296/// Test conditional classes
297pub fn test_conditional_classes(
298    actual_classes: &HashSet<String>,
299    expected_conditional: &[(&str, &str)],
300) -> ClassTestResult {
301    let mut missing_classes = HashSet::new();
302    let mut found_classes = HashSet::new();
303
304    for (condition, class) in expected_conditional {
305        let conditional_class = format!("{}:{}", condition, class);
306        if actual_classes.contains(&conditional_class) {
307            found_classes.insert(conditional_class);
308        } else {
309            missing_classes.insert(conditional_class);
310        }
311    }
312
313    if missing_classes.is_empty() {
314        ClassTestResult::success(
315            "All expected conditional classes found",
316            expected_conditional
317                .iter()
318                .map(|(cond, cls)| format!("{}:{}", cond, cls))
319                .collect(),
320            actual_classes.clone(),
321        )
322    } else {
323        ClassTestResult::failure(
324            format!("Missing conditional classes: {:?}", missing_classes),
325            expected_conditional
326                .iter()
327                .map(|(cond, cls)| format!("{}:{}", cond, cls))
328                .collect(),
329            actual_classes.clone(),
330            missing_classes,
331            HashSet::new(),
332        )
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn test_test_classes_success() {
342        let actual: HashSet<String> = ["bg-blue-500", "text-white"]
343            .iter()
344            .map(|s| s.to_string())
345            .collect();
346        let expected: HashSet<String> = ["bg-blue-500", "text-white"]
347            .iter()
348            .map(|s| s.to_string())
349            .collect();
350
351        let result = test_classes(&actual, &expected);
352        assert!(result.success);
353        assert!(result.missing_classes.is_empty());
354        assert!(result.extra_classes.is_empty());
355    }
356
357    #[test]
358    fn test_test_classes_missing() {
359        let actual: HashSet<String> = ["bg-blue-500"].iter().map(|s| s.to_string()).collect();
360        let expected: HashSet<String> = ["bg-blue-500", "text-white"]
361            .iter()
362            .map(|s| s.to_string())
363            .collect();
364
365        let result = test_classes(&actual, &expected);
366        assert!(!result.success);
367        assert!(result.missing_classes.contains("text-white"));
368        assert!(result.extra_classes.is_empty());
369    }
370
371    #[test]
372    fn test_test_classes_extra() {
373        let actual: HashSet<String> = ["bg-blue-500", "text-white", "extra-class"]
374            .iter()
375            .map(|s| s.to_string())
376            .collect();
377        let expected: HashSet<String> = ["bg-blue-500", "text-white"]
378            .iter()
379            .map(|s| s.to_string())
380            .collect();
381
382        let result = test_classes(&actual, &expected);
383        assert!(!result.success);
384        assert!(result.missing_classes.is_empty());
385        assert!(result.extra_classes.contains("extra-class"));
386    }
387
388    #[test]
389    fn test_test_classes_contain() {
390        let actual: HashSet<String> = ["bg-blue-500", "text-white", "extra-class"]
391            .iter()
392            .map(|s| s.to_string())
393            .collect();
394        let expected: HashSet<String> = ["bg-blue-500", "text-white"]
395            .iter()
396            .map(|s| s.to_string())
397            .collect();
398
399        let result = test_classes_contain(&actual, &expected);
400        assert!(result.success);
401        assert!(result.missing_classes.is_empty());
402    }
403
404    #[test]
405    fn test_test_classes_not_contain() {
406        let actual: HashSet<String> = ["bg-blue-500", "text-white"]
407            .iter()
408            .map(|s| s.to_string())
409            .collect();
410        let unexpected: HashSet<String> = ["bg-red-500", "text-black"]
411            .iter()
412            .map(|s| s.to_string())
413            .collect();
414
415        let result = test_classes_not_contain(&actual, &unexpected);
416        assert!(result.success);
417        assert!(result.extra_classes.is_empty());
418    }
419
420    #[test]
421    fn test_test_classes_any() {
422        let actual: HashSet<String> = ["bg-blue-500", "text-white"]
423            .iter()
424            .map(|s| s.to_string())
425            .collect();
426        let expected: HashSet<String> = ["bg-red-500", "bg-blue-500"]
427            .iter()
428            .map(|s| s.to_string())
429            .collect();
430
431        let result = test_classes_any(&actual, &expected);
432        assert!(result.success);
433    }
434
435    #[test]
436    fn test_test_responsive_classes() {
437        let actual: HashSet<String> = ["sm:text-sm", "md:text-md"]
438            .iter()
439            .map(|s| s.to_string())
440            .collect();
441        let expected = [("sm", "text-sm"), ("md", "text-md")];
442
443        let result = test_responsive_classes(&actual, &expected);
444        assert!(result.success);
445    }
446
447    #[test]
448    fn test_test_conditional_classes() {
449        let actual: HashSet<String> = ["hover:bg-blue-600", "focus:ring-2"]
450            .iter()
451            .map(|s| s.to_string())
452            .collect();
453        let expected = [("hover", "bg-blue-600"), ("focus", "ring-2")];
454
455        let result = test_conditional_classes(&actual, &expected);
456        assert!(result.success);
457    }
458}