1use crate::bridge::VisualDiff;
33use crate::driver::Screenshot;
34use crate::result::{ProbarError, ProbarResult};
35use serde::{Deserialize, Serialize};
36use std::path::Path;
37use std::time::{Duration, SystemTime};
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum FailureMode {
45 #[default]
47 AndonCord,
48 CollectAll,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
54pub enum TestStatus {
55 Passed,
57 Failed,
59 Skipped,
61 Pending,
63}
64
65impl TestStatus {
66 #[must_use]
68 pub const fn is_passed(&self) -> bool {
69 matches!(self, Self::Passed)
70 }
71
72 #[must_use]
74 pub const fn is_failed(&self) -> bool {
75 matches!(self, Self::Failed)
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct TestResultEntry {
82 pub name: String,
84 pub status: TestStatus,
86 pub duration: Duration,
88 pub error: Option<String>,
90 #[serde(skip)]
92 pub failure_screenshot: Option<Screenshot>,
93 pub stack_trace: Option<String>,
95 pub timestamp: SystemTime,
97}
98
99impl TestResultEntry {
100 #[must_use]
102 pub fn passed(name: impl Into<String>, duration: Duration) -> Self {
103 Self {
104 name: name.into(),
105 status: TestStatus::Passed,
106 duration,
107 error: None,
108 failure_screenshot: None,
109 stack_trace: None,
110 timestamp: SystemTime::now(),
111 }
112 }
113
114 #[must_use]
116 pub fn failed(name: impl Into<String>, duration: Duration, error: impl Into<String>) -> Self {
117 Self {
118 name: name.into(),
119 status: TestStatus::Failed,
120 duration,
121 error: Some(error.into()),
122 failure_screenshot: None,
123 stack_trace: None,
124 timestamp: SystemTime::now(),
125 }
126 }
127
128 #[must_use]
130 pub fn skipped(name: impl Into<String>) -> Self {
131 Self {
132 name: name.into(),
133 status: TestStatus::Skipped,
134 duration: Duration::ZERO,
135 error: None,
136 failure_screenshot: None,
137 stack_trace: None,
138 timestamp: SystemTime::now(),
139 }
140 }
141
142 #[must_use]
144 pub fn with_screenshot(mut self, screenshot: Screenshot) -> Self {
145 self.failure_screenshot = Some(screenshot);
146 self
147 }
148
149 #[must_use]
151 pub fn with_stack_trace(mut self, trace: impl Into<String>) -> Self {
152 self.stack_trace = Some(trace.into());
153 self
154 }
155}
156
157#[derive(Debug, Clone, Default, Serialize, Deserialize)]
159pub struct TraceData {
160 pub total_duration: Duration,
162 pub step_timings: Vec<(String, Duration)>,
164 pub memory_samples: Vec<(Duration, u64)>,
166 pub fps_samples: Vec<(Duration, f64)>,
168}
169
170impl TraceData {
171 #[must_use]
173 pub fn new() -> Self {
174 Self::default()
175 }
176
177 pub fn add_step(&mut self, name: impl Into<String>, duration: Duration) {
179 self.step_timings.push((name.into(), duration));
180 }
181
182 pub fn add_memory_sample(&mut self, elapsed: Duration, bytes: u64) {
184 self.memory_samples.push((elapsed, bytes));
185 }
186
187 pub fn add_fps_sample(&mut self, elapsed: Duration, fps: f64) {
189 self.fps_samples.push((elapsed, fps));
190 }
191
192 #[must_use]
194 pub fn average_fps(&self) -> f64 {
195 if self.fps_samples.is_empty() {
196 return 0.0;
197 }
198 let sum: f64 = self.fps_samples.iter().map(|(_, fps)| fps).sum();
199 sum / self.fps_samples.len() as f64
200 }
201
202 #[must_use]
204 pub fn peak_memory(&self) -> u64 {
205 self.memory_samples
206 .iter()
207 .map(|(_, mem)| *mem)
208 .max()
209 .unwrap_or(0)
210 }
211}
212
213#[derive(Debug)]
217pub struct AndonCordPulled {
218 pub test_name: String,
220 pub failure: String,
222 pub screenshot: Option<Screenshot>,
224}
225
226#[derive(Debug, Default)]
241pub struct Reporter {
242 results: Vec<TestResultEntry>,
244 screenshots: Vec<(String, Screenshot)>,
246 visual_diffs: Vec<(String, VisualDiff)>,
248 traces: Vec<TraceData>,
250 failure_mode: FailureMode,
252 suite_name: String,
254 start_time: Option<SystemTime>,
256}
257
258impl Reporter {
259 #[must_use]
261 pub fn new() -> Self {
262 Self {
263 suite_name: "Test Suite".to_string(),
264 ..Default::default()
265 }
266 }
267
268 #[must_use]
270 pub fn andon() -> Self {
271 Self {
272 failure_mode: FailureMode::AndonCord,
273 suite_name: "Test Suite".to_string(),
274 ..Default::default()
275 }
276 }
277
278 #[must_use]
280 pub fn collect_all() -> Self {
281 Self {
282 failure_mode: FailureMode::CollectAll,
283 suite_name: "Test Suite".to_string(),
284 ..Default::default()
285 }
286 }
287
288 #[must_use]
290 pub fn with_name(mut self, name: impl Into<String>) -> Self {
291 self.suite_name = name.into();
292 self
293 }
294
295 pub fn start(&mut self) {
297 self.start_time = Some(SystemTime::now());
298 }
299
300 pub fn record(&mut self, result: TestResultEntry) -> ProbarResult<()> {
306 let failed = result.status.is_failed();
307 let failure_info = if failed {
308 Some((
309 result.name.clone(),
310 result.error.clone().unwrap_or_default(),
311 ))
312 } else {
313 None
314 };
315
316 self.results.push(result);
317
318 if self.failure_mode == FailureMode::AndonCord {
319 if let Some((test_name, failure)) = failure_info {
320 return Err(ProbarError::AssertionFailed {
322 message: format!("ANDON CORD PULLED: Test '{test_name}' failed: {failure}"),
323 });
324 }
325 }
326
327 Ok(())
328 }
329
330 pub fn add_screenshot(&mut self, name: impl Into<String>, screenshot: Screenshot) {
332 self.screenshots.push((name.into(), screenshot));
333 }
334
335 pub fn add_visual_diff(&mut self, name: impl Into<String>, diff: VisualDiff) {
337 self.visual_diffs.push((name.into(), diff));
338 }
339
340 pub fn add_trace(&mut self, trace: TraceData) {
342 self.traces.push(trace);
343 }
344
345 #[must_use]
347 pub fn passed_count(&self) -> usize {
348 self.results.iter().filter(|r| r.status.is_passed()).count()
349 }
350
351 #[must_use]
353 pub fn failed_count(&self) -> usize {
354 self.results.iter().filter(|r| r.status.is_failed()).count()
355 }
356
357 #[must_use]
359 pub fn total_count(&self) -> usize {
360 self.results.len()
361 }
362
363 #[must_use]
365 pub fn pass_rate(&self) -> f64 {
366 if self.results.is_empty() {
367 return 1.0;
368 }
369 self.passed_count() as f64 / self.results.len() as f64
370 }
371
372 #[must_use]
374 pub fn all_passed(&self) -> bool {
375 self.failed_count() == 0
376 }
377
378 #[must_use]
380 pub fn total_duration(&self) -> Duration {
381 self.results.iter().map(|r| r.duration).sum()
382 }
383
384 #[must_use]
386 pub fn results(&self) -> &[TestResultEntry] {
387 &self.results
388 }
389
390 #[must_use]
392 pub fn failures(&self) -> Vec<&TestResultEntry> {
393 self.results
394 .iter()
395 .filter(|r| r.status.is_failed())
396 .collect()
397 }
398
399 #[must_use]
401 pub fn summary(&self) -> String {
402 format!(
403 "{}: {}/{} passed ({:.1}%)",
404 self.suite_name,
405 self.passed_count(),
406 self.total_count(),
407 self.pass_rate() * 100.0
408 )
409 }
410
411 pub fn generate_html(&self, output_path: &Path) -> ProbarResult<()> {
417 let html = self.render_html();
418 std::fs::write(output_path, html)?;
419 Ok(())
420 }
421
422 #[must_use]
424 pub fn render_html(&self) -> String {
425 let mut html = String::new();
426
427 html.push_str(r#"<!DOCTYPE html>
429<html>
430<head>
431 <meta charset="UTF-8">
432 <title>Probar Test Report</title>
433 <style>
434 body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 20px; }
435 .summary { background: #f5f5f5; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
436 .progress-bar { background: #ddd; height: 20px; border-radius: 10px; overflow: hidden; }
437 .passed { background: #4caf50; height: 100%; }
438 .test { padding: 10px; margin: 5px 0; border-radius: 4px; }
439 .test.pass { background: #e8f5e9; border-left: 4px solid #4caf50; }
440 .test.fail { background: #ffebee; border-left: 4px solid #f44336; }
441 .test.skip { background: #fff3e0; border-left: 4px solid #ff9800; }
442 .error { color: #d32f2f; font-family: monospace; white-space: pre-wrap; }
443 .visual-diff { display: flex; gap: 10px; margin: 10px 0; }
444 .visual-diff img { max-width: 300px; border: 1px solid #ddd; }
445 </style>
446</head>
447<body>
448"#);
449
450 html.push_str(&format!(
452 r#"<div class="summary">
453 <h1>{}</h1>
454 <h2>Results: {}/{} passed ({:.1}%)</h2>
455 <div class="progress-bar">
456 <div class="passed" style="width: {:.1}%"></div>
457 </div>
458 <p>Duration: {:.2}s</p>
459</div>
460"#,
461 self.suite_name,
462 self.passed_count(),
463 self.total_count(),
464 self.pass_rate() * 100.0,
465 self.pass_rate() * 100.0,
466 self.total_duration().as_secs_f64()
467 ));
468
469 html.push_str("<h2>Test Results</h2>\n");
471 for result in &self.results {
472 let class = match result.status {
473 TestStatus::Passed => "pass",
474 TestStatus::Failed => "fail",
475 TestStatus::Skipped | TestStatus::Pending => "skip",
476 };
477
478 html.push_str(&format!(
479 r#"<div class="test {}">
480 <strong>{}</strong> - {:?} ({:.2}ms)
481"#,
482 class,
483 result.name,
484 result.status,
485 result.duration.as_secs_f64() * 1000.0
486 ));
487
488 if let Some(error) = &result.error {
489 html.push_str(&format!(r#" <div class="error">{error}</div>"#));
490 }
491
492 html.push_str("</div>\n");
493 }
494
495 if !self.visual_diffs.is_empty() {
497 html.push_str("<h2>Visual Differences</h2>\n");
498 for (name, diff) in &self.visual_diffs {
499 html.push_str(&format!(
500 r#"<div>
501 <h3>{}</h3>
502 <p>Similarity: {:.1}%</p>
503 <div class="visual-diff">
504 <div><strong>Expected</strong><br><img alt="Expected"></div>
505 <div><strong>Actual</strong><br><img alt="Actual"></div>
506 <div><strong>Diff</strong><br><img alt="Diff"></div>
507 </div>
508</div>
509"#,
510 name,
511 diff.perceptual_similarity * 100.0
512 ));
513 }
514 }
515
516 html.push_str(
518 r#"
519<footer>
520 <p>Generated by Probar - WASM Game Testing Framework</p>
521</footer>
522</body>
523</html>
524"#,
525 );
526
527 html
528 }
529
530 pub fn generate_junit(&self, output_path: &Path) -> ProbarResult<()> {
536 let xml = self.render_junit();
537 std::fs::write(output_path, xml)?;
538 Ok(())
539 }
540
541 #[must_use]
543 pub fn render_junit(&self) -> String {
544 let mut xml = String::new();
545
546 xml.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
547 xml.push('\n');
548 xml.push_str(&format!(
549 r#"<testsuite name="{}" tests="{}" failures="{}" time="{:.3}">"#,
550 self.suite_name,
551 self.total_count(),
552 self.failed_count(),
553 self.total_duration().as_secs_f64()
554 ));
555 xml.push('\n');
556
557 for result in &self.results {
558 xml.push_str(&format!(
559 r#" <testcase name="{}" time="{:.3}">"#,
560 result.name,
561 result.duration.as_secs_f64()
562 ));
563 xml.push('\n');
564
565 if let Some(error) = &result.error {
566 xml.push_str(&format!(
567 r#" <failure message="{}">{}</failure>"#,
568 escape_xml(error),
569 escape_xml(error)
570 ));
571 xml.push('\n');
572 }
573
574 xml.push_str(" </testcase>\n");
575 }
576
577 xml.push_str("</testsuite>\n");
578 xml
579 }
580}
581
582fn escape_xml(s: &str) -> String {
584 s.replace('&', "&")
585 .replace('<', "<")
586 .replace('>', ">")
587 .replace('"', """)
588 .replace('\'', "'")
589}
590
591#[cfg(test)]
596#[allow(clippy::unwrap_used, clippy::expect_used)]
597mod tests {
598 use super::*;
599
600 mod failure_mode_tests {
601 use super::*;
602
603 #[test]
604 fn test_default_failure_mode() {
605 let mode = FailureMode::default();
606 assert_eq!(mode, FailureMode::AndonCord);
607 }
608 }
609
610 mod test_status_tests {
611 use super::*;
612
613 #[test]
614 fn test_status_is_passed() {
615 assert!(TestStatus::Passed.is_passed());
616 assert!(!TestStatus::Failed.is_passed());
617 assert!(!TestStatus::Skipped.is_passed());
618 }
619
620 #[test]
621 fn test_status_is_failed() {
622 assert!(!TestStatus::Passed.is_failed());
623 assert!(TestStatus::Failed.is_failed());
624 assert!(!TestStatus::Skipped.is_failed());
625 }
626 }
627
628 mod test_result_entry_tests {
629 use super::*;
630
631 #[test]
632 fn test_passed_result() {
633 let result = TestResultEntry::passed("test_1", Duration::from_millis(100));
634 assert_eq!(result.name, "test_1");
635 assert_eq!(result.status, TestStatus::Passed);
636 assert!(result.error.is_none());
637 }
638
639 #[test]
640 fn test_failed_result() {
641 let result =
642 TestResultEntry::failed("test_2", Duration::from_millis(50), "assertion failed");
643 assert_eq!(result.name, "test_2");
644 assert_eq!(result.status, TestStatus::Failed);
645 assert_eq!(result.error, Some("assertion failed".to_string()));
646 }
647
648 #[test]
649 fn test_skipped_result() {
650 let result = TestResultEntry::skipped("test_3");
651 assert_eq!(result.status, TestStatus::Skipped);
652 assert_eq!(result.duration, Duration::ZERO);
653 }
654
655 #[test]
656 fn test_with_stack_trace() {
657 let result = TestResultEntry::failed("test", Duration::ZERO, "error")
658 .with_stack_trace("at line 42");
659 assert_eq!(result.stack_trace, Some("at line 42".to_string()));
660 }
661 }
662
663 mod trace_data_tests {
664 use super::*;
665
666 #[test]
667 fn test_new_trace() {
668 let trace = TraceData::new();
669 assert!(trace.step_timings.is_empty());
670 assert!(trace.memory_samples.is_empty());
671 }
672
673 #[test]
674 fn test_add_step() {
675 let mut trace = TraceData::new();
676 trace.add_step("setup", Duration::from_millis(10));
677 assert_eq!(trace.step_timings.len(), 1);
678 }
679
680 #[test]
681 fn test_average_fps() {
682 let mut trace = TraceData::new();
683 trace.add_fps_sample(Duration::ZERO, 60.0);
684 trace.add_fps_sample(Duration::from_secs(1), 50.0);
685 trace.add_fps_sample(Duration::from_secs(2), 55.0);
686 assert!((trace.average_fps() - 55.0).abs() < f64::EPSILON);
687 }
688
689 #[test]
690 fn test_average_fps_empty() {
691 let trace = TraceData::new();
692 assert!((trace.average_fps() - 0.0).abs() < f64::EPSILON);
693 }
694
695 #[test]
696 fn test_peak_memory() {
697 let mut trace = TraceData::new();
698 trace.add_memory_sample(Duration::ZERO, 1000);
699 trace.add_memory_sample(Duration::from_secs(1), 5000);
700 trace.add_memory_sample(Duration::from_secs(2), 3000);
701 assert_eq!(trace.peak_memory(), 5000);
702 }
703 }
704
705 mod reporter_tests {
706 use super::*;
707
708 #[test]
709 fn test_new_reporter() {
710 let reporter = Reporter::new();
711 assert_eq!(reporter.total_count(), 0);
712 assert!(reporter.all_passed());
713 }
714
715 #[test]
716 fn test_andon_reporter() {
717 let reporter = Reporter::andon();
718 assert_eq!(reporter.failure_mode, FailureMode::AndonCord);
719 }
720
721 #[test]
722 fn test_collect_all_reporter() {
723 let reporter = Reporter::collect_all();
724 assert_eq!(reporter.failure_mode, FailureMode::CollectAll);
725 }
726
727 #[test]
728 fn test_with_name() {
729 let reporter = Reporter::new().with_name("My Tests");
730 assert_eq!(reporter.suite_name, "My Tests");
731 }
732
733 #[test]
734 fn test_record_passing() {
735 let mut reporter = Reporter::andon();
736 let result = reporter.record(TestResultEntry::passed("test", Duration::ZERO));
737 assert!(result.is_ok());
738 assert_eq!(reporter.passed_count(), 1);
739 }
740
741 #[test]
742 fn test_andon_cord_pulled() {
743 let mut reporter = Reporter::andon();
744 let result = reporter.record(TestResultEntry::failed("test", Duration::ZERO, "error"));
745 assert!(result.is_err());
746 }
747
748 #[test]
749 fn test_collect_all_continues() {
750 let mut reporter = Reporter::collect_all();
751 let result1 =
752 reporter.record(TestResultEntry::failed("test1", Duration::ZERO, "error"));
753 let result2 = reporter.record(TestResultEntry::passed("test2", Duration::ZERO));
754 assert!(result1.is_ok()); assert!(result2.is_ok());
756 assert_eq!(reporter.failed_count(), 1);
757 assert_eq!(reporter.passed_count(), 1);
758 }
759
760 #[test]
761 fn test_pass_rate() {
762 let mut reporter = Reporter::collect_all();
763 reporter
764 .record(TestResultEntry::passed("t1", Duration::ZERO))
765 .unwrap();
766 reporter
767 .record(TestResultEntry::passed("t2", Duration::ZERO))
768 .unwrap();
769 reporter
770 .record(TestResultEntry::failed("t3", Duration::ZERO, "err"))
771 .unwrap();
772 reporter
773 .record(TestResultEntry::passed("t4", Duration::ZERO))
774 .unwrap();
775
776 assert!((reporter.pass_rate() - 0.75).abs() < f64::EPSILON);
777 }
778
779 #[test]
780 fn test_pass_rate_empty() {
781 let reporter = Reporter::new();
782 assert!((reporter.pass_rate() - 1.0).abs() < f64::EPSILON);
783 }
784
785 #[test]
786 fn test_total_duration() {
787 let mut reporter = Reporter::collect_all();
788 reporter
789 .record(TestResultEntry::passed("t1", Duration::from_millis(100)))
790 .unwrap();
791 reporter
792 .record(TestResultEntry::passed("t2", Duration::from_millis(200)))
793 .unwrap();
794 assert_eq!(reporter.total_duration(), Duration::from_millis(300));
795 }
796
797 #[test]
798 fn test_failures() {
799 let mut reporter = Reporter::collect_all();
800 reporter
801 .record(TestResultEntry::passed("t1", Duration::ZERO))
802 .unwrap();
803 reporter
804 .record(TestResultEntry::failed("t2", Duration::ZERO, "err"))
805 .unwrap();
806 reporter
807 .record(TestResultEntry::passed("t3", Duration::ZERO))
808 .unwrap();
809
810 let failures = reporter.failures();
811 assert_eq!(failures.len(), 1);
812 assert_eq!(failures[0].name, "t2");
813 }
814
815 #[test]
816 fn test_summary() {
817 let mut reporter = Reporter::collect_all().with_name("Game Tests");
818 reporter
819 .record(TestResultEntry::passed("t1", Duration::ZERO))
820 .unwrap();
821 reporter
822 .record(TestResultEntry::passed("t2", Duration::ZERO))
823 .unwrap();
824
825 let summary = reporter.summary();
826 assert!(summary.contains("Game Tests"));
827 assert!(summary.contains("2/2"));
828 assert!(summary.contains("100.0%"));
829 }
830
831 #[test]
832 fn test_render_html() {
833 let mut reporter = Reporter::collect_all().with_name("HTML Test");
834 reporter
835 .record(TestResultEntry::passed("t1", Duration::from_millis(50)))
836 .unwrap();
837 reporter
838 .record(TestResultEntry::failed(
839 "t2",
840 Duration::from_millis(10),
841 "assertion failed",
842 ))
843 .unwrap();
844
845 let html = reporter.render_html();
846 assert!(html.contains("HTML Test"));
847 assert!(html.contains("t1"));
848 assert!(html.contains("t2"));
849 assert!(html.contains("assertion failed"));
850 }
851
852 #[test]
853 fn test_render_junit() {
854 let mut reporter = Reporter::collect_all().with_name("JUnit Test");
855 reporter
856 .record(TestResultEntry::passed(
857 "passing_test",
858 Duration::from_millis(100),
859 ))
860 .unwrap();
861 reporter
862 .record(TestResultEntry::failed(
863 "failing_test",
864 Duration::from_millis(50),
865 "error msg",
866 ))
867 .unwrap();
868
869 let xml = reporter.render_junit();
870 assert!(xml.contains("JUnit Test"));
871 assert!(xml.contains("passing_test"));
872 assert!(xml.contains("failing_test"));
873 assert!(xml.contains("error msg"));
874 }
875 }
876
877 mod escape_xml_tests {
878 use super::*;
879
880 #[test]
881 fn test_escape_special_chars() {
882 assert_eq!(escape_xml("a & b"), "a & b");
883 assert_eq!(escape_xml("<tag>"), "<tag>");
884 assert_eq!(escape_xml("\"quoted\""), ""quoted"");
885 assert_eq!(escape_xml("it's"), "it's");
886 }
887
888 #[test]
889 fn test_no_escape_needed() {
890 assert_eq!(escape_xml("plain text"), "plain text");
891 }
892 }
893
894 mod additional_coverage_tests {
895 use super::*;
896 use tempfile::tempdir;
897
898 #[test]
899 fn test_with_screenshot() {
900 let screenshot = Screenshot::new(vec![1, 2, 3], 100, 100);
901 let result = TestResultEntry::failed("test", Duration::ZERO, "error")
902 .with_screenshot(screenshot);
903 assert!(result.failure_screenshot.is_some());
904 }
905
906 #[test]
907 fn test_reporter_start() {
908 let mut reporter = Reporter::new();
909 assert!(reporter.start_time.is_none());
910 reporter.start();
911 assert!(reporter.start_time.is_some());
912 }
913
914 #[test]
915 fn test_reporter_add_screenshot() {
916 let mut reporter = Reporter::new();
917 let screenshot = Screenshot::new(vec![1, 2, 3], 100, 100);
918 reporter.add_screenshot("test_shot", screenshot);
919 assert_eq!(reporter.screenshots.len(), 1);
920 }
921
922 #[test]
923 fn test_reporter_add_visual_diff() {
924 let mut reporter = Reporter::new();
925 let diff = VisualDiff::new(0.95, vec![1, 2, 3]);
926 reporter.add_visual_diff("test_diff", diff);
927 assert_eq!(reporter.visual_diffs.len(), 1);
928 }
929
930 #[test]
931 fn test_reporter_add_trace() {
932 let mut reporter = Reporter::new();
933 let mut trace = TraceData::new();
934 trace.add_step("step1", Duration::from_millis(100));
935 reporter.add_trace(trace);
936 assert_eq!(reporter.traces.len(), 1);
937 }
938
939 #[test]
940 fn test_reporter_results_accessor() {
941 let mut reporter = Reporter::collect_all();
942 reporter
943 .record(TestResultEntry::passed("t1", Duration::ZERO))
944 .unwrap();
945 reporter
946 .record(TestResultEntry::passed("t2", Duration::ZERO))
947 .unwrap();
948
949 let results = reporter.results();
950 assert_eq!(results.len(), 2);
951 assert_eq!(results[0].name, "t1");
952 assert_eq!(results[1].name, "t2");
953 }
954
955 #[test]
956 fn test_generate_html_to_file() {
957 let mut reporter = Reporter::collect_all().with_name("File Test");
958 reporter
959 .record(TestResultEntry::passed("t1", Duration::ZERO))
960 .unwrap();
961
962 let dir = tempdir().unwrap();
963 let path = dir.path().join("report.html");
964
965 let result = reporter.generate_html(&path);
966 assert!(result.is_ok());
967 assert!(path.exists());
968
969 let content = std::fs::read_to_string(&path).unwrap();
970 assert!(content.contains("File Test"));
971 }
972
973 #[test]
974 fn test_generate_junit_to_file() {
975 let mut reporter = Reporter::collect_all().with_name("JUnit File Test");
976 reporter
977 .record(TestResultEntry::passed("t1", Duration::ZERO))
978 .unwrap();
979
980 let dir = tempdir().unwrap();
981 let path = dir.path().join("report.xml");
982
983 let result = reporter.generate_junit(&path);
984 assert!(result.is_ok());
985 assert!(path.exists());
986
987 let content = std::fs::read_to_string(&path).unwrap();
988 assert!(content.contains("JUnit File Test"));
989 }
990
991 #[test]
992 fn test_render_html_with_visual_diffs() {
993 let mut reporter = Reporter::collect_all().with_name("Visual Test");
994 reporter
995 .record(TestResultEntry::passed("t1", Duration::ZERO))
996 .unwrap();
997
998 let diff = VisualDiff::new(0.85, vec![1, 2, 3]);
999 reporter.add_visual_diff("homepage", diff);
1000
1001 let html = reporter.render_html();
1002 assert!(html.contains("Visual Differences"));
1003 assert!(html.contains("homepage"));
1004 assert!(html.contains("85.0%")); }
1006 }
1007}