1#![allow(clippy::must_use_candidate)]
12#![allow(clippy::missing_panics_doc)]
13#![allow(clippy::missing_errors_doc)]
14#![allow(clippy::module_name_repetitions)]
15#![allow(clippy::missing_const_for_fn)]
16#![allow(clippy::struct_excessive_bools)]
17#![allow(clippy::cast_possible_truncation)]
18#![allow(clippy::cast_precision_loss)]
19#![allow(clippy::io_other_error)]
20#![allow(clippy::if_not_else)]
21#![allow(clippy::format_push_string)]
22#![allow(clippy::uninlined_format_args)]
23
24use serde::{Deserialize, Serialize};
25use std::path::PathBuf;
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
33#[serde(tag = "type")]
34pub enum RecordedEvent {
35 Click {
37 x: i32,
39 y: i32,
41 selector: Option<String>,
43 timestamp_ms: u64,
45 },
46 KeyPress {
48 key: String,
50 modifiers: KeyModifiers,
52 timestamp_ms: u64,
54 },
55 TextInput {
57 text: String,
59 selector: Option<String>,
61 timestamp_ms: u64,
63 },
64 NetworkComplete {
66 url: String,
68 status: u16,
70 duration_ms: u64,
72 timestamp_ms: u64,
74 },
75 WasmLoaded {
77 url: String,
79 size: u64,
81 timestamp_ms: u64,
83 },
84 StateChange {
86 from: String,
88 to: String,
90 event: String,
92 timestamp_ms: u64,
94 },
95 Assertion {
97 name: String,
99 passed: bool,
101 actual: String,
103 expected: String,
105 timestamp_ms: u64,
107 },
108}
109
110#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112pub struct KeyModifiers {
113 pub ctrl: bool,
115 pub alt: bool,
117 pub shift: bool,
119 pub meta: bool,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct Recording {
126 pub version: String,
128 pub name: String,
130 pub url: String,
132 pub user_agent: String,
134 pub viewport: Viewport,
136 pub start_time: u64,
138 pub duration_ms: u64,
140 pub events: Vec<RecordedEvent>,
142 pub metadata: RecordingMetadata,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct Viewport {
149 pub width: u32,
151 pub height: u32,
153 pub device_pixel_ratio: f32,
155}
156
157impl Default for Viewport {
158 fn default() -> Self {
159 Self {
160 width: 1920,
161 height: 1080,
162 device_pixel_ratio: 1.0,
163 }
164 }
165}
166
167#[derive(Debug, Clone, Default, Serialize, Deserialize)]
169pub struct RecordingMetadata {
170 pub commit: Option<String>,
172 pub test_name: Option<String>,
174 pub description: Option<String>,
176}
177
178impl Recording {
179 pub fn new(name: impl Into<String>, url: impl Into<String>) -> Self {
181 use std::time::{SystemTime, UNIX_EPOCH};
182
183 Self {
184 version: "1.0.0".to_string(),
185 name: name.into(),
186 url: url.into(),
187 user_agent: String::new(),
188 viewport: Viewport::default(),
189 start_time: SystemTime::now()
190 .duration_since(UNIX_EPOCH)
191 .map(|d| d.as_millis() as u64)
192 .unwrap_or(0),
193 duration_ms: 0,
194 events: Vec::new(),
195 metadata: RecordingMetadata::default(),
196 }
197 }
198
199 pub fn add_event(&mut self, event: RecordedEvent) {
201 self.events.push(event);
202 }
203
204 pub fn event_count(&self) -> usize {
206 self.events.len()
207 }
208
209 pub fn save(&self, path: &PathBuf) -> std::io::Result<()> {
211 let json = serde_json::to_string_pretty(self)
212 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
213 std::fs::write(path, json)
214 }
215
216 pub fn load(path: &PathBuf) -> std::io::Result<Self> {
218 let content = std::fs::read_to_string(path)?;
219 serde_json::from_str(&content)
220 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
221 }
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct MemorySnapshot {
231 pub heap_bytes: u64,
233 pub timestamp_ms: u64,
235 pub label: Option<String>,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct MemoryProfile {
242 pub module_name: String,
244 pub initial_heap: u64,
246 pub peak_heap: u64,
248 pub current_heap: u64,
250 pub snapshots: Vec<MemorySnapshot>,
252 pub growth_events: Vec<MemoryGrowthEvent>,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct MemoryGrowthEvent {
259 pub from_bytes: u64,
261 pub to_bytes: u64,
263 pub timestamp_ms: u64,
265 pub reason: Option<String>,
267}
268
269impl MemoryProfile {
270 pub fn new(module_name: impl Into<String>, initial_heap: u64) -> Self {
272 Self {
273 module_name: module_name.into(),
274 initial_heap,
275 peak_heap: initial_heap,
276 current_heap: initial_heap,
277 snapshots: vec![MemorySnapshot {
278 heap_bytes: initial_heap,
279 timestamp_ms: 0,
280 label: Some("initial".to_string()),
281 }],
282 growth_events: Vec::new(),
283 }
284 }
285
286 pub fn snapshot(&mut self, heap_bytes: u64, timestamp_ms: u64, label: Option<String>) {
288 if heap_bytes > self.current_heap {
289 self.growth_events.push(MemoryGrowthEvent {
290 from_bytes: self.current_heap,
291 to_bytes: heap_bytes,
292 timestamp_ms,
293 reason: label.clone(),
294 });
295 }
296
297 self.current_heap = heap_bytes;
298 if heap_bytes > self.peak_heap {
299 self.peak_heap = heap_bytes;
300 }
301
302 self.snapshots.push(MemorySnapshot {
303 heap_bytes,
304 timestamp_ms,
305 label,
306 });
307 }
308
309 pub fn exceeds_threshold(&self, threshold_bytes: u64) -> bool {
311 self.peak_heap > threshold_bytes
312 }
313
314 pub fn growth_percentage(&self) -> f64 {
316 if self.initial_heap == 0 {
317 return 0.0;
318 }
319 ((self.peak_heap - self.initial_heap) as f64 / self.initial_heap as f64) * 100.0
320 }
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
329#[serde(rename_all = "lowercase")]
330pub enum Browser {
331 Chrome,
333 Firefox,
335 Safari,
337 IosSafari,
339 ChromeAndroid,
341}
342
343impl Browser {
344 pub const fn name(&self) -> &'static str {
346 match self {
347 Self::Chrome => "Chrome",
348 Self::Firefox => "Firefox",
349 Self::Safari => "Safari",
350 Self::IosSafari => "iOS Safari",
351 Self::ChromeAndroid => "Chrome Android",
352 }
353 }
354
355 pub const fn engine(&self) -> &'static str {
357 match self {
358 Self::Chrome | Self::ChromeAndroid => "Chromium",
359 Self::Firefox => "Gecko",
360 Self::Safari | Self::IosSafari => "WebKit",
361 }
362 }
363
364 pub fn desktop_browsers() -> Vec<Self> {
366 vec![Self::Chrome, Self::Firefox, Self::Safari]
367 }
368
369 pub fn mobile_browsers() -> Vec<Self> {
371 vec![Self::IosSafari, Self::ChromeAndroid]
372 }
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct BrowserMatrix {
378 pub browsers: Vec<Browser>,
380 pub viewports: Vec<Viewport>,
382 pub parallel: bool,
384}
385
386impl Default for BrowserMatrix {
387 fn default() -> Self {
388 Self {
389 browsers: Browser::desktop_browsers(),
390 viewports: vec![
391 Viewport {
392 width: 1920,
393 height: 1080,
394 device_pixel_ratio: 1.0,
395 },
396 Viewport {
397 width: 1280,
398 height: 720,
399 device_pixel_ratio: 1.0,
400 },
401 Viewport {
402 width: 375,
403 height: 667,
404 device_pixel_ratio: 2.0,
405 }, ],
407 parallel: true,
408 }
409 }
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct BrowserTestResult {
415 pub browser: Browser,
417 pub viewport: Viewport,
419 pub passed: bool,
421 pub duration_ms: u64,
423 pub error: Option<String>,
425 pub screenshots: Vec<String>,
427}
428
429#[derive(Debug, Clone, Serialize, Deserialize)]
435pub struct PerformanceMetric {
436 pub name: String,
438 pub value: f64,
440 pub unit: String,
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize)]
446pub struct PerformanceBaseline {
447 pub version: String,
449 pub commit: String,
451 pub timestamp: u64,
453 pub metrics: Vec<PerformanceMetric>,
455}
456
457impl PerformanceBaseline {
458 pub fn new(commit: impl Into<String>) -> Self {
460 use std::time::{SystemTime, UNIX_EPOCH};
461
462 Self {
463 version: "1.0.0".to_string(),
464 commit: commit.into(),
465 timestamp: SystemTime::now()
466 .duration_since(UNIX_EPOCH)
467 .map(|d| d.as_secs())
468 .unwrap_or(0),
469 metrics: Vec::new(),
470 }
471 }
472
473 pub fn add_metric(&mut self, name: impl Into<String>, value: f64, unit: impl Into<String>) {
475 self.metrics.push(PerformanceMetric {
476 name: name.into(),
477 value,
478 unit: unit.into(),
479 });
480 }
481
482 pub fn save(&self, path: &PathBuf) -> std::io::Result<()> {
484 let json = serde_json::to_string_pretty(self)
485 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
486 std::fs::write(path, json)
487 }
488
489 pub fn load(path: &PathBuf) -> std::io::Result<Self> {
491 let content = std::fs::read_to_string(path)?;
492 serde_json::from_str(&content)
493 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
494 }
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct PerformanceComparison {
500 pub name: String,
502 pub baseline: f64,
504 pub current: f64,
506 pub change_percent: f64,
508 pub status: ComparisonStatus,
510}
511
512#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
514#[serde(rename_all = "lowercase")]
515pub enum ComparisonStatus {
516 Ok,
518 Warn,
520 Fail,
522}
523
524impl ComparisonStatus {
525 pub const fn symbol(&self) -> &'static str {
527 match self {
528 Self::Ok => "✓",
529 Self::Warn => "⚠",
530 Self::Fail => "✗",
531 }
532 }
533}
534
535pub fn compare_performance(
537 baseline: &PerformanceBaseline,
538 current: &[PerformanceMetric],
539 threshold_percent: f64,
540) -> Vec<PerformanceComparison> {
541 let mut results = Vec::new();
542
543 for current_metric in current {
544 if let Some(baseline_metric) = baseline
545 .metrics
546 .iter()
547 .find(|m| m.name == current_metric.name)
548 {
549 let change = if baseline_metric.value != 0.0 {
550 ((current_metric.value - baseline_metric.value) / baseline_metric.value) * 100.0
551 } else {
552 0.0
553 };
554
555 let status = if change.abs() > threshold_percent {
556 ComparisonStatus::Fail
557 } else if change.abs() > threshold_percent * 0.8 {
558 ComparisonStatus::Warn
559 } else {
560 ComparisonStatus::Ok
561 };
562
563 results.push(PerformanceComparison {
564 name: current_metric.name.clone(),
565 baseline: baseline_metric.value,
566 current: current_metric.value,
567 change_percent: change,
568 status,
569 });
570 }
571 }
572
573 results
574}
575
576pub fn render_performance_report(
578 baseline: &PerformanceBaseline,
579 comparisons: &[PerformanceComparison],
580) -> String {
581 let mut output = String::new();
582
583 output.push_str("PERFORMANCE REGRESSION CHECK\n");
584 output.push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n");
585 output.push_str(&format!(
586 "Baseline: {} (commit {})\n\n",
587 baseline.version,
588 &baseline.commit[..8.min(baseline.commit.len())]
589 ));
590
591 output.push_str("┌────────────────────┬──────────┬──────────┬──────────┬────────┐\n");
592 output.push_str("│ Metric │ Baseline │ Current │ Delta │ Status │\n");
593 output.push_str("├────────────────────┼──────────┼──────────┼──────────┼────────┤\n");
594
595 for comp in comparisons {
596 let delta = if comp.change_percent >= 0.0 {
597 format!("+{:.1}%", comp.change_percent)
598 } else {
599 format!("{:.1}%", comp.change_percent)
600 };
601
602 output.push_str(&format!(
603 "│ {:<18} │ {:>8.1} │ {:>8.1} │ {:>8} │ {} {:>4} │\n",
604 comp.name,
605 comp.baseline,
606 comp.current,
607 delta,
608 comp.status.symbol(),
609 match comp.status {
610 ComparisonStatus::Ok => "OK",
611 ComparisonStatus::Warn => "WARN",
612 ComparisonStatus::Fail => "FAIL",
613 }
614 ));
615 }
616
617 output.push_str("└────────────────────┴──────────┴──────────┴──────────┴────────┘\n");
618
619 let warnings = comparisons
620 .iter()
621 .filter(|c| c.status == ComparisonStatus::Warn)
622 .count();
623 let failures = comparisons
624 .iter()
625 .filter(|c| c.status == ComparisonStatus::Fail)
626 .count();
627
628 output.push_str(&format!(
629 "\nResult: {} warnings, {} failures\n",
630 warnings, failures
631 ));
632
633 output
634}
635
636#[cfg(test)]
637mod tests {
638 use super::*;
639
640 #[test]
642 fn test_recording_new() {
643 let recording = Recording::new("test", "http://localhost:8080");
644 assert_eq!(recording.name, "test");
645 assert_eq!(recording.url, "http://localhost:8080");
646 assert_eq!(recording.event_count(), 0);
647 }
648
649 #[test]
650 fn test_recording_add_event() {
651 let mut recording = Recording::new("test", "http://localhost");
652 recording.add_event(RecordedEvent::Click {
653 x: 100,
654 y: 200,
655 selector: Some("#button".to_string()),
656 timestamp_ms: 1000,
657 });
658 assert_eq!(recording.event_count(), 1);
659 }
660
661 #[test]
662 fn test_key_modifiers_default() {
663 let mods = KeyModifiers::default();
664 assert!(!mods.ctrl);
665 assert!(!mods.alt);
666 assert!(!mods.shift);
667 assert!(!mods.meta);
668 }
669
670 #[test]
672 fn test_memory_profile_new() {
673 let profile = MemoryProfile::new("test_module", 1024 * 1024);
674 assert_eq!(profile.initial_heap, 1024 * 1024);
675 assert_eq!(profile.peak_heap, 1024 * 1024);
676 }
677
678 #[test]
679 fn test_memory_profile_snapshot() {
680 let mut profile = MemoryProfile::new("test", 1000);
681 profile.snapshot(2000, 100, Some("allocation".to_string()));
682
683 assert_eq!(profile.current_heap, 2000);
684 assert_eq!(profile.peak_heap, 2000);
685 assert_eq!(profile.snapshots.len(), 2);
686 assert_eq!(profile.growth_events.len(), 1);
687 }
688
689 #[test]
690 fn test_memory_profile_threshold() {
691 let mut profile = MemoryProfile::new("test", 100);
692 profile.snapshot(500, 100, None);
693
694 assert!(profile.exceeds_threshold(400));
695 assert!(!profile.exceeds_threshold(600));
696 }
697
698 #[test]
699 fn test_memory_profile_growth_percentage() {
700 let mut profile = MemoryProfile::new("test", 100);
701 profile.snapshot(200, 100, None);
702
703 assert!((profile.growth_percentage() - 100.0).abs() < 0.1);
704 }
705
706 #[test]
708 fn test_browser_name() {
709 assert_eq!(Browser::Chrome.name(), "Chrome");
710 assert_eq!(Browser::Firefox.name(), "Firefox");
711 assert_eq!(Browser::Safari.name(), "Safari");
712 }
713
714 #[test]
715 fn test_browser_engine() {
716 assert_eq!(Browser::Chrome.engine(), "Chromium");
717 assert_eq!(Browser::Firefox.engine(), "Gecko");
718 assert_eq!(Browser::Safari.engine(), "WebKit");
719 }
720
721 #[test]
722 fn test_browser_matrix_default() {
723 let matrix = BrowserMatrix::default();
724 assert_eq!(matrix.browsers.len(), 3);
725 assert_eq!(matrix.viewports.len(), 3);
726 assert!(matrix.parallel);
727 }
728
729 #[test]
731 fn test_performance_baseline_new() {
732 let baseline = PerformanceBaseline::new("abc123");
733 assert_eq!(baseline.commit, "abc123");
734 assert!(baseline.metrics.is_empty());
735 }
736
737 #[test]
738 fn test_performance_baseline_add_metric() {
739 let mut baseline = PerformanceBaseline::new("abc123");
740 baseline.add_metric("rtf", 1.5, "x");
741 baseline.add_metric("latency_p95", 45.0, "ms");
742
743 assert_eq!(baseline.metrics.len(), 2);
744 }
745
746 #[test]
747 fn test_compare_performance_ok() {
748 let mut baseline = PerformanceBaseline::new("old");
749 baseline.add_metric("latency", 100.0, "ms");
750
751 let current = vec![PerformanceMetric {
752 name: "latency".to_string(),
753 value: 105.0,
754 unit: "ms".to_string(),
755 }];
756
757 let results = compare_performance(&baseline, ¤t, 10.0);
758 assert_eq!(results.len(), 1);
759 assert_eq!(results[0].status, ComparisonStatus::Ok);
760 }
761
762 #[test]
763 fn test_compare_performance_warn() {
764 let mut baseline = PerformanceBaseline::new("old");
765 baseline.add_metric("latency", 100.0, "ms");
766
767 let current = vec![PerformanceMetric {
768 name: "latency".to_string(),
769 value: 109.0,
770 unit: "ms".to_string(),
771 }];
772
773 let results = compare_performance(&baseline, ¤t, 10.0);
774 assert_eq!(results[0].status, ComparisonStatus::Warn);
775 }
776
777 #[test]
778 fn test_compare_performance_fail() {
779 let mut baseline = PerformanceBaseline::new("old");
780 baseline.add_metric("latency", 100.0, "ms");
781
782 let current = vec![PerformanceMetric {
783 name: "latency".to_string(),
784 value: 115.0,
785 unit: "ms".to_string(),
786 }];
787
788 let results = compare_performance(&baseline, ¤t, 10.0);
789 assert_eq!(results[0].status, ComparisonStatus::Fail);
790 }
791
792 #[test]
793 fn test_comparison_status_symbol() {
794 assert_eq!(ComparisonStatus::Ok.symbol(), "✓");
795 assert_eq!(ComparisonStatus::Warn.symbol(), "⚠");
796 assert_eq!(ComparisonStatus::Fail.symbol(), "✗");
797 }
798
799 #[test]
800 fn test_render_performance_report() {
801 let mut baseline = PerformanceBaseline::new("abc12345");
802 baseline.add_metric("latency", 100.0, "ms");
803
804 let comparisons = vec![PerformanceComparison {
805 name: "latency".to_string(),
806 baseline: 100.0,
807 current: 105.0,
808 change_percent: 5.0,
809 status: ComparisonStatus::Ok,
810 }];
811
812 let output = render_performance_report(&baseline, &comparisons);
813 assert!(output.contains("PERFORMANCE REGRESSION"));
814 assert!(output.contains("latency"));
815 assert!(output.contains("+5.0%"));
816 }
817
818 #[test]
819 fn test_viewport_default() {
820 let vp = Viewport::default();
821 assert_eq!(vp.width, 1920);
822 assert_eq!(vp.height, 1080);
823 }
824
825 #[test]
826 fn test_recording_save_load() {
827 let mut recording = Recording::new("test-session", "http://localhost/test");
828 recording.add_event(RecordedEvent::Click {
829 x: 100,
830 y: 200,
831 selector: Some("#button".to_string()),
832 timestamp_ms: 1000,
833 });
834 recording.add_event(RecordedEvent::KeyPress {
835 key: "Enter".to_string(),
836 modifiers: KeyModifiers::default(),
837 timestamp_ms: 2000,
838 });
839
840 let temp_dir = std::env::temp_dir();
841 let path = temp_dir.join("test_recording.json");
842
843 recording.save(&path).expect("Failed to save recording");
845
846 let loaded = Recording::load(&path).expect("Failed to load recording");
848 assert_eq!(loaded.name, "test-session");
849 assert_eq!(loaded.event_count(), 2);
850
851 let _ = std::fs::remove_file(&path);
853 }
854
855 #[test]
856 fn test_recording_load_nonexistent() {
857 let result = Recording::load(&PathBuf::from("/nonexistent/path.json"));
858 assert!(result.is_err());
859 }
860
861 #[test]
864 fn test_recorded_event_all_variants() {
865 let text_input = RecordedEvent::TextInput {
867 text: "hello".to_string(),
868 selector: Some("#input".to_string()),
869 timestamp_ms: 100,
870 };
871 assert!(matches!(text_input, RecordedEvent::TextInput { .. }));
872
873 let network = RecordedEvent::NetworkComplete {
875 url: "https://api.example.com".to_string(),
876 status: 200,
877 duration_ms: 50,
878 timestamp_ms: 200,
879 };
880 assert!(matches!(network, RecordedEvent::NetworkComplete { .. }));
881
882 let wasm = RecordedEvent::WasmLoaded {
884 url: "/game.wasm".to_string(),
885 size: 1024000,
886 timestamp_ms: 300,
887 };
888 assert!(matches!(wasm, RecordedEvent::WasmLoaded { .. }));
889
890 let state_change = RecordedEvent::StateChange {
892 from: "menu".to_string(),
893 to: "game".to_string(),
894 event: "start_clicked".to_string(),
895 timestamp_ms: 400,
896 };
897 assert!(matches!(state_change, RecordedEvent::StateChange { .. }));
898
899 let assertion = RecordedEvent::Assertion {
901 name: "score_check".to_string(),
902 passed: true,
903 actual: "100".to_string(),
904 expected: "100".to_string(),
905 timestamp_ms: 500,
906 };
907 assert!(matches!(assertion, RecordedEvent::Assertion { .. }));
908 }
909
910 #[test]
911 fn test_browser_ios_safari() {
912 assert_eq!(Browser::IosSafari.name(), "iOS Safari");
913 assert_eq!(Browser::IosSafari.engine(), "WebKit");
914 }
915
916 #[test]
917 fn test_browser_chrome_android() {
918 assert_eq!(Browser::ChromeAndroid.name(), "Chrome Android");
919 assert_eq!(Browser::ChromeAndroid.engine(), "Chromium");
920 }
921
922 #[test]
923 fn test_browser_desktop_browsers() {
924 let browsers = Browser::desktop_browsers();
925 assert_eq!(browsers.len(), 3);
926 assert!(browsers.contains(&Browser::Chrome));
927 assert!(browsers.contains(&Browser::Firefox));
928 assert!(browsers.contains(&Browser::Safari));
929 }
930
931 #[test]
932 fn test_browser_mobile_browsers() {
933 let browsers = Browser::mobile_browsers();
934 assert_eq!(browsers.len(), 2);
935 assert!(browsers.contains(&Browser::IosSafari));
936 assert!(browsers.contains(&Browser::ChromeAndroid));
937 }
938
939 #[test]
940 fn test_browser_test_result() {
941 let result = BrowserTestResult {
942 browser: Browser::Chrome,
943 viewport: Viewport::default(),
944 passed: true,
945 duration_ms: 1500,
946 error: None,
947 screenshots: vec!["screenshot1.png".to_string()],
948 };
949 assert!(result.passed);
950 assert!(result.error.is_none());
951 assert_eq!(result.screenshots.len(), 1);
952 }
953
954 #[test]
955 fn test_browser_test_result_failed() {
956 let result = BrowserTestResult {
957 browser: Browser::Firefox,
958 viewport: Viewport {
959 width: 1280,
960 height: 720,
961 device_pixel_ratio: 1.0,
962 },
963 passed: false,
964 duration_ms: 500,
965 error: Some("Element not found".to_string()),
966 screenshots: vec![],
967 };
968 assert!(!result.passed);
969 assert!(result.error.is_some());
970 }
971
972 #[test]
973 fn test_memory_profile_growth_zero_initial() {
974 let profile = MemoryProfile::new("test", 0);
975 assert_eq!(profile.growth_percentage(), 0.0);
976 }
977
978 #[test]
979 fn test_memory_profile_no_growth() {
980 let mut profile = MemoryProfile::new("test", 1000);
981 profile.snapshot(800, 100, Some("shrink".to_string()));
982
983 assert_eq!(profile.current_heap, 800);
984 assert_eq!(profile.peak_heap, 1000);
985 assert!(profile.growth_events.is_empty());
986 }
987
988 #[test]
989 fn test_compare_performance_zero_baseline() {
990 let mut baseline = PerformanceBaseline::new("old");
991 baseline.add_metric("count", 0.0, "n");
992
993 let current = vec![PerformanceMetric {
994 name: "count".to_string(),
995 value: 10.0,
996 unit: "n".to_string(),
997 }];
998
999 let results = compare_performance(&baseline, ¤t, 10.0);
1000 assert_eq!(results.len(), 1);
1001 assert_eq!(results[0].change_percent, 0.0);
1002 }
1003
1004 #[test]
1005 fn test_compare_performance_no_match() {
1006 let mut baseline = PerformanceBaseline::new("old");
1007 baseline.add_metric("latency", 100.0, "ms");
1008
1009 let current = vec![PerformanceMetric {
1010 name: "throughput".to_string(),
1011 value: 500.0,
1012 unit: "req/s".to_string(),
1013 }];
1014
1015 let results = compare_performance(&baseline, ¤t, 10.0);
1016 assert!(results.is_empty());
1017 }
1018
1019 #[test]
1020 fn test_render_performance_report_negative_change() {
1021 let mut baseline = PerformanceBaseline::new("abc12345");
1022 baseline.add_metric("latency", 100.0, "ms");
1023
1024 let comparisons = vec![PerformanceComparison {
1025 name: "latency".to_string(),
1026 baseline: 100.0,
1027 current: 90.0,
1028 change_percent: -10.0,
1029 status: ComparisonStatus::Ok,
1030 }];
1031
1032 let output = render_performance_report(&baseline, &comparisons);
1033 assert!(output.contains("-10.0%"));
1034 }
1035
1036 #[test]
1037 fn test_render_performance_report_warnings_and_failures() {
1038 let baseline = PerformanceBaseline::new("abc12345");
1039
1040 let comparisons = vec![
1041 PerformanceComparison {
1042 name: "metric1".to_string(),
1043 baseline: 100.0,
1044 current: 109.0,
1045 change_percent: 9.0,
1046 status: ComparisonStatus::Warn,
1047 },
1048 PerformanceComparison {
1049 name: "metric2".to_string(),
1050 baseline: 100.0,
1051 current: 120.0,
1052 change_percent: 20.0,
1053 status: ComparisonStatus::Fail,
1054 },
1055 ];
1056
1057 let output = render_performance_report(&baseline, &comparisons);
1058 assert!(output.contains("1 warnings"));
1059 assert!(output.contains("1 failures"));
1060 }
1061
1062 #[test]
1063 fn test_recording_metadata() {
1064 let mut recording = Recording::new("test", "http://localhost");
1065 recording.metadata = RecordingMetadata {
1066 commit: Some("abc123".to_string()),
1067 test_name: Some("login_test".to_string()),
1068 description: Some("Tests the login flow".to_string()),
1069 };
1070
1071 assert_eq!(recording.metadata.commit, Some("abc123".to_string()));
1072 assert_eq!(recording.metadata.test_name, Some("login_test".to_string()));
1073 }
1074
1075 #[test]
1076 fn test_recording_serde() {
1077 let mut recording = Recording::new("serde_test", "http://localhost");
1078 recording.add_event(RecordedEvent::Click {
1079 x: 10,
1080 y: 20,
1081 selector: None,
1082 timestamp_ms: 0,
1083 });
1084
1085 let json = serde_json::to_string(&recording).unwrap();
1086 let parsed: Recording = serde_json::from_str(&json).unwrap();
1087
1088 assert_eq!(parsed.name, "serde_test");
1089 assert_eq!(parsed.event_count(), 1);
1090 }
1091
1092 #[test]
1093 fn test_performance_baseline_save_load() {
1094 let mut baseline = PerformanceBaseline::new("commit123");
1095 baseline.add_metric("rtf", 1.5, "x");
1096 baseline.add_metric("fps", 60.0, "fps");
1097
1098 let temp_dir = std::env::temp_dir();
1099 let path = temp_dir.join("test_baseline.json");
1100
1101 baseline.save(&path).expect("Failed to save");
1102
1103 let loaded = PerformanceBaseline::load(&path).expect("Failed to load");
1104 assert_eq!(loaded.commit, "commit123");
1105 assert_eq!(loaded.metrics.len(), 2);
1106
1107 let _ = std::fs::remove_file(&path);
1108 }
1109
1110 #[test]
1111 fn test_performance_baseline_load_nonexistent() {
1112 let result = PerformanceBaseline::load(&PathBuf::from("/nonexistent/baseline.json"));
1113 assert!(result.is_err());
1114 }
1115
1116 #[test]
1117 fn test_key_modifiers_all_true() {
1118 let mods = KeyModifiers {
1119 ctrl: true,
1120 alt: true,
1121 shift: true,
1122 meta: true,
1123 };
1124 assert!(mods.ctrl);
1125 assert!(mods.alt);
1126 assert!(mods.shift);
1127 assert!(mods.meta);
1128 }
1129
1130 #[test]
1131 fn test_browser_eq() {
1132 assert_eq!(Browser::Chrome, Browser::Chrome);
1133 assert_ne!(Browser::Chrome, Browser::Firefox);
1134 }
1135
1136 #[test]
1137 fn test_comparison_status_eq() {
1138 assert_eq!(ComparisonStatus::Ok, ComparisonStatus::Ok);
1139 assert_ne!(ComparisonStatus::Ok, ComparisonStatus::Fail);
1140 }
1141}