1use std::collections::HashSet;
4
5#[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 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 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
52pub 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
94pub 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
121pub 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
148pub 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
156pub 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
186pub 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
194pub 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
202pub 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
213pub 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
224pub 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
232pub 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
240pub 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
248pub 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
256pub 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
296pub 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}