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_or(0, |d| d.as_millis() as u64),
192 duration_ms: 0,
193 events: Vec::new(),
194 metadata: RecordingMetadata::default(),
195 }
196 }
197
198 pub fn add_event(&mut self, event: RecordedEvent) {
200 self.events.push(event);
201 }
202
203 pub fn event_count(&self) -> usize {
205 self.events.len()
206 }
207
208 pub fn save(&self, path: &PathBuf) -> std::io::Result<()> {
210 let json = serde_json::to_string_pretty(self)
211 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
212 std::fs::write(path, json)
213 }
214
215 pub fn load(path: &PathBuf) -> std::io::Result<Self> {
217 let content = std::fs::read_to_string(path)?;
218 serde_json::from_str(&content)
219 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
220 }
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct MemorySnapshot {
230 pub heap_bytes: u64,
232 pub timestamp_ms: u64,
234 pub label: Option<String>,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct MemoryProfile {
241 pub module_name: String,
243 pub initial_heap: u64,
245 pub peak_heap: u64,
247 pub current_heap: u64,
249 pub snapshots: Vec<MemorySnapshot>,
251 pub growth_events: Vec<MemoryGrowthEvent>,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct MemoryGrowthEvent {
258 pub from_bytes: u64,
260 pub to_bytes: u64,
262 pub timestamp_ms: u64,
264 pub reason: Option<String>,
266}
267
268impl MemoryProfile {
269 pub fn new(module_name: impl Into<String>, initial_heap: u64) -> Self {
271 Self {
272 module_name: module_name.into(),
273 initial_heap,
274 peak_heap: initial_heap,
275 current_heap: initial_heap,
276 snapshots: vec![MemorySnapshot {
277 heap_bytes: initial_heap,
278 timestamp_ms: 0,
279 label: Some("initial".to_string()),
280 }],
281 growth_events: Vec::new(),
282 }
283 }
284
285 pub fn snapshot(&mut self, heap_bytes: u64, timestamp_ms: u64, label: Option<String>) {
287 if heap_bytes > self.current_heap {
288 self.growth_events.push(MemoryGrowthEvent {
289 from_bytes: self.current_heap,
290 to_bytes: heap_bytes,
291 timestamp_ms,
292 reason: label.clone(),
293 });
294 }
295
296 self.current_heap = heap_bytes;
297 if heap_bytes > self.peak_heap {
298 self.peak_heap = heap_bytes;
299 }
300
301 self.snapshots.push(MemorySnapshot {
302 heap_bytes,
303 timestamp_ms,
304 label,
305 });
306 }
307
308 pub fn exceeds_threshold(&self, threshold_bytes: u64) -> bool {
310 self.peak_heap > threshold_bytes
311 }
312
313 pub fn growth_percentage(&self) -> f64 {
315 if self.initial_heap == 0 {
316 return 0.0;
317 }
318 ((self.peak_heap - self.initial_heap) as f64 / self.initial_heap as f64) * 100.0
319 }
320}
321
322#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
328#[serde(rename_all = "lowercase")]
329pub enum Browser {
330 Chrome,
332 Firefox,
334 Safari,
336 IosSafari,
338 ChromeAndroid,
340}
341
342impl Browser {
343 pub const fn name(&self) -> &'static str {
345 match self {
346 Self::Chrome => "Chrome",
347 Self::Firefox => "Firefox",
348 Self::Safari => "Safari",
349 Self::IosSafari => "iOS Safari",
350 Self::ChromeAndroid => "Chrome Android",
351 }
352 }
353
354 pub const fn engine(&self) -> &'static str {
356 match self {
357 Self::Chrome | Self::ChromeAndroid => "Chromium",
358 Self::Firefox => "Gecko",
359 Self::Safari | Self::IosSafari => "WebKit",
360 }
361 }
362
363 pub fn desktop_browsers() -> Vec<Self> {
365 vec![Self::Chrome, Self::Firefox, Self::Safari]
366 }
367
368 pub fn mobile_browsers() -> Vec<Self> {
370 vec![Self::IosSafari, Self::ChromeAndroid]
371 }
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct BrowserMatrix {
377 pub browsers: Vec<Browser>,
379 pub viewports: Vec<Viewport>,
381 pub parallel: bool,
383}
384
385impl Default for BrowserMatrix {
386 fn default() -> Self {
387 Self {
388 browsers: Browser::desktop_browsers(),
389 viewports: vec![
390 Viewport {
391 width: 1920,
392 height: 1080,
393 device_pixel_ratio: 1.0,
394 },
395 Viewport {
396 width: 1280,
397 height: 720,
398 device_pixel_ratio: 1.0,
399 },
400 Viewport {
401 width: 375,
402 height: 667,
403 device_pixel_ratio: 2.0,
404 }, ],
406 parallel: true,
407 }
408 }
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct BrowserTestResult {
414 pub browser: Browser,
416 pub viewport: Viewport,
418 pub passed: bool,
420 pub duration_ms: u64,
422 pub error: Option<String>,
424 pub screenshots: Vec<String>,
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct PerformanceMetric {
435 pub name: String,
437 pub value: f64,
439 pub unit: String,
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct PerformanceBaseline {
446 pub version: String,
448 pub commit: String,
450 pub timestamp: u64,
452 pub metrics: Vec<PerformanceMetric>,
454}
455
456impl PerformanceBaseline {
457 pub fn new(commit: impl Into<String>) -> Self {
459 use std::time::{SystemTime, UNIX_EPOCH};
460
461 Self {
462 version: "1.0.0".to_string(),
463 commit: commit.into(),
464 timestamp: SystemTime::now()
465 .duration_since(UNIX_EPOCH)
466 .map_or(0, |d| d.as_secs()),
467 metrics: Vec::new(),
468 }
469 }
470
471 pub fn add_metric(&mut self, name: impl Into<String>, value: f64, unit: impl Into<String>) {
473 self.metrics.push(PerformanceMetric {
474 name: name.into(),
475 value,
476 unit: unit.into(),
477 });
478 }
479
480 pub fn save(&self, path: &PathBuf) -> std::io::Result<()> {
482 let json = serde_json::to_string_pretty(self)
483 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
484 std::fs::write(path, json)
485 }
486
487 pub fn load(path: &PathBuf) -> std::io::Result<Self> {
489 let content = std::fs::read_to_string(path)?;
490 serde_json::from_str(&content)
491 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
492 }
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize)]
497pub struct PerformanceComparison {
498 pub name: String,
500 pub baseline: f64,
502 pub current: f64,
504 pub change_percent: f64,
506 pub status: ComparisonStatus,
508}
509
510#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
512#[serde(rename_all = "lowercase")]
513pub enum ComparisonStatus {
514 Ok,
516 Warn,
518 Fail,
520}
521
522impl ComparisonStatus {
523 pub const fn symbol(&self) -> &'static str {
525 match self {
526 Self::Ok => "✓",
527 Self::Warn => "⚠",
528 Self::Fail => "✗",
529 }
530 }
531}
532
533pub fn compare_performance(
535 baseline: &PerformanceBaseline,
536 current: &[PerformanceMetric],
537 threshold_percent: f64,
538) -> Vec<PerformanceComparison> {
539 let mut results = Vec::new();
540
541 for current_metric in current {
542 if let Some(baseline_metric) = baseline
543 .metrics
544 .iter()
545 .find(|m| m.name == current_metric.name)
546 {
547 let change = if baseline_metric.value != 0.0 {
548 ((current_metric.value - baseline_metric.value) / baseline_metric.value) * 100.0
549 } else {
550 0.0
551 };
552
553 let status = if change.abs() > threshold_percent {
554 ComparisonStatus::Fail
555 } else if change.abs() > threshold_percent * 0.8 {
556 ComparisonStatus::Warn
557 } else {
558 ComparisonStatus::Ok
559 };
560
561 results.push(PerformanceComparison {
562 name: current_metric.name.clone(),
563 baseline: baseline_metric.value,
564 current: current_metric.value,
565 change_percent: change,
566 status,
567 });
568 }
569 }
570
571 results
572}
573
574pub fn render_performance_report(
576 baseline: &PerformanceBaseline,
577 comparisons: &[PerformanceComparison],
578) -> String {
579 let mut output = String::new();
580
581 output.push_str("PERFORMANCE REGRESSION CHECK\n");
582 output.push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n");
583 output.push_str(&format!(
584 "Baseline: {} (commit {})\n\n",
585 baseline.version,
586 &baseline.commit[..8.min(baseline.commit.len())]
587 ));
588
589 output.push_str("┌────────────────────┬──────────┬──────────┬──────────┬────────┐\n");
590 output.push_str("│ Metric │ Baseline │ Current │ Delta │ Status │\n");
591 output.push_str("├────────────────────┼──────────┼──────────┼──────────┼────────┤\n");
592
593 for comp in comparisons {
594 let delta = if comp.change_percent >= 0.0 {
595 format!("+{:.1}%", comp.change_percent)
596 } else {
597 format!("{:.1}%", comp.change_percent)
598 };
599
600 output.push_str(&format!(
601 "│ {:<18} │ {:>8.1} │ {:>8.1} │ {:>8} │ {} {:>4} │\n",
602 comp.name,
603 comp.baseline,
604 comp.current,
605 delta,
606 comp.status.symbol(),
607 match comp.status {
608 ComparisonStatus::Ok => "OK",
609 ComparisonStatus::Warn => "WARN",
610 ComparisonStatus::Fail => "FAIL",
611 }
612 ));
613 }
614
615 output.push_str("└────────────────────┴──────────┴──────────┴──────────┴────────┘\n");
616
617 let warnings = comparisons
618 .iter()
619 .filter(|c| c.status == ComparisonStatus::Warn)
620 .count();
621 let failures = comparisons
622 .iter()
623 .filter(|c| c.status == ComparisonStatus::Fail)
624 .count();
625
626 output.push_str(&format!(
627 "\nResult: {} warnings, {} failures\n",
628 warnings, failures
629 ));
630
631 output
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637
638 #[test]
640 fn test_recording_new() {
641 let recording = Recording::new("test", "http://localhost:8080");
642 assert_eq!(recording.name, "test");
643 assert_eq!(recording.url, "http://localhost:8080");
644 assert_eq!(recording.event_count(), 0);
645 }
646
647 #[test]
648 fn test_recording_add_event() {
649 let mut recording = Recording::new("test", "http://localhost");
650 recording.add_event(RecordedEvent::Click {
651 x: 100,
652 y: 200,
653 selector: Some("#button".to_string()),
654 timestamp_ms: 1000,
655 });
656 assert_eq!(recording.event_count(), 1);
657 }
658
659 #[test]
660 fn test_key_modifiers_default() {
661 let mods = KeyModifiers::default();
662 assert!(!mods.ctrl);
663 assert!(!mods.alt);
664 assert!(!mods.shift);
665 assert!(!mods.meta);
666 }
667
668 #[test]
670 fn test_memory_profile_new() {
671 let profile = MemoryProfile::new("test_module", 1024 * 1024);
672 assert_eq!(profile.initial_heap, 1024 * 1024);
673 assert_eq!(profile.peak_heap, 1024 * 1024);
674 }
675
676 #[test]
677 fn test_memory_profile_snapshot() {
678 let mut profile = MemoryProfile::new("test", 1000);
679 profile.snapshot(2000, 100, Some("allocation".to_string()));
680
681 assert_eq!(profile.current_heap, 2000);
682 assert_eq!(profile.peak_heap, 2000);
683 assert_eq!(profile.snapshots.len(), 2);
684 assert_eq!(profile.growth_events.len(), 1);
685 }
686
687 #[test]
688 fn test_memory_profile_threshold() {
689 let mut profile = MemoryProfile::new("test", 100);
690 profile.snapshot(500, 100, None);
691
692 assert!(profile.exceeds_threshold(400));
693 assert!(!profile.exceeds_threshold(600));
694 }
695
696 #[test]
697 fn test_memory_profile_growth_percentage() {
698 let mut profile = MemoryProfile::new("test", 100);
699 profile.snapshot(200, 100, None);
700
701 assert!((profile.growth_percentage() - 100.0).abs() < 0.1);
702 }
703
704 #[test]
706 fn test_browser_name() {
707 assert_eq!(Browser::Chrome.name(), "Chrome");
708 assert_eq!(Browser::Firefox.name(), "Firefox");
709 assert_eq!(Browser::Safari.name(), "Safari");
710 }
711
712 #[test]
713 fn test_browser_engine() {
714 assert_eq!(Browser::Chrome.engine(), "Chromium");
715 assert_eq!(Browser::Firefox.engine(), "Gecko");
716 assert_eq!(Browser::Safari.engine(), "WebKit");
717 }
718
719 #[test]
720 fn test_browser_matrix_default() {
721 let matrix = BrowserMatrix::default();
722 assert_eq!(matrix.browsers.len(), 3);
723 assert_eq!(matrix.viewports.len(), 3);
724 assert!(matrix.parallel);
725 }
726
727 #[test]
729 fn test_performance_baseline_new() {
730 let baseline = PerformanceBaseline::new("abc123");
731 assert_eq!(baseline.commit, "abc123");
732 assert!(baseline.metrics.is_empty());
733 }
734
735 #[test]
736 fn test_performance_baseline_add_metric() {
737 let mut baseline = PerformanceBaseline::new("abc123");
738 baseline.add_metric("rtf", 1.5, "x");
739 baseline.add_metric("latency_p95", 45.0, "ms");
740
741 assert_eq!(baseline.metrics.len(), 2);
742 }
743
744 #[test]
745 fn test_compare_performance_ok() {
746 let mut baseline = PerformanceBaseline::new("old");
747 baseline.add_metric("latency", 100.0, "ms");
748
749 let current = vec![PerformanceMetric {
750 name: "latency".to_string(),
751 value: 105.0,
752 unit: "ms".to_string(),
753 }];
754
755 let results = compare_performance(&baseline, ¤t, 10.0);
756 assert_eq!(results.len(), 1);
757 assert_eq!(results[0].status, ComparisonStatus::Ok);
758 }
759
760 #[test]
761 fn test_compare_performance_warn() {
762 let mut baseline = PerformanceBaseline::new("old");
763 baseline.add_metric("latency", 100.0, "ms");
764
765 let current = vec![PerformanceMetric {
766 name: "latency".to_string(),
767 value: 109.0,
768 unit: "ms".to_string(),
769 }];
770
771 let results = compare_performance(&baseline, ¤t, 10.0);
772 assert_eq!(results[0].status, ComparisonStatus::Warn);
773 }
774
775 #[test]
776 fn test_compare_performance_fail() {
777 let mut baseline = PerformanceBaseline::new("old");
778 baseline.add_metric("latency", 100.0, "ms");
779
780 let current = vec![PerformanceMetric {
781 name: "latency".to_string(),
782 value: 115.0,
783 unit: "ms".to_string(),
784 }];
785
786 let results = compare_performance(&baseline, ¤t, 10.0);
787 assert_eq!(results[0].status, ComparisonStatus::Fail);
788 }
789
790 #[test]
791 fn test_comparison_status_symbol() {
792 assert_eq!(ComparisonStatus::Ok.symbol(), "✓");
793 assert_eq!(ComparisonStatus::Warn.symbol(), "⚠");
794 assert_eq!(ComparisonStatus::Fail.symbol(), "✗");
795 }
796
797 #[test]
798 fn test_render_performance_report() {
799 let mut baseline = PerformanceBaseline::new("abc12345");
800 baseline.add_metric("latency", 100.0, "ms");
801
802 let comparisons = vec![PerformanceComparison {
803 name: "latency".to_string(),
804 baseline: 100.0,
805 current: 105.0,
806 change_percent: 5.0,
807 status: ComparisonStatus::Ok,
808 }];
809
810 let output = render_performance_report(&baseline, &comparisons);
811 assert!(output.contains("PERFORMANCE REGRESSION"));
812 assert!(output.contains("latency"));
813 assert!(output.contains("+5.0%"));
814 }
815
816 #[test]
817 fn test_viewport_default() {
818 let vp = Viewport::default();
819 assert_eq!(vp.width, 1920);
820 assert_eq!(vp.height, 1080);
821 }
822
823 #[test]
824 fn test_recording_save_load() {
825 let mut recording = Recording::new("test-session", "http://localhost/test");
826 recording.add_event(RecordedEvent::Click {
827 x: 100,
828 y: 200,
829 selector: Some("#button".to_string()),
830 timestamp_ms: 1000,
831 });
832 recording.add_event(RecordedEvent::KeyPress {
833 key: "Enter".to_string(),
834 modifiers: KeyModifiers::default(),
835 timestamp_ms: 2000,
836 });
837
838 let temp_dir = std::env::temp_dir();
839 let path = temp_dir.join("test_recording.json");
840
841 recording.save(&path).expect("Failed to save recording");
843
844 let loaded = Recording::load(&path).expect("Failed to load recording");
846 assert_eq!(loaded.name, "test-session");
847 assert_eq!(loaded.event_count(), 2);
848
849 let _ = std::fs::remove_file(&path);
851 }
852
853 #[test]
854 fn test_recording_load_nonexistent() {
855 let result = Recording::load(&PathBuf::from("/nonexistent/path.json"));
856 assert!(result.is_err());
857 }
858
859 #[test]
862 fn test_recorded_event_all_variants() {
863 let text_input = RecordedEvent::TextInput {
865 text: "hello".to_string(),
866 selector: Some("#input".to_string()),
867 timestamp_ms: 100,
868 };
869 assert!(matches!(text_input, RecordedEvent::TextInput { .. }));
870
871 let network = RecordedEvent::NetworkComplete {
873 url: "https://api.example.com".to_string(),
874 status: 200,
875 duration_ms: 50,
876 timestamp_ms: 200,
877 };
878 assert!(matches!(network, RecordedEvent::NetworkComplete { .. }));
879
880 let wasm = RecordedEvent::WasmLoaded {
882 url: "/game.wasm".to_string(),
883 size: 1024000,
884 timestamp_ms: 300,
885 };
886 assert!(matches!(wasm, RecordedEvent::WasmLoaded { .. }));
887
888 let state_change = RecordedEvent::StateChange {
890 from: "menu".to_string(),
891 to: "game".to_string(),
892 event: "start_clicked".to_string(),
893 timestamp_ms: 400,
894 };
895 assert!(matches!(state_change, RecordedEvent::StateChange { .. }));
896
897 let assertion = RecordedEvent::Assertion {
899 name: "score_check".to_string(),
900 passed: true,
901 actual: "100".to_string(),
902 expected: "100".to_string(),
903 timestamp_ms: 500,
904 };
905 assert!(matches!(assertion, RecordedEvent::Assertion { .. }));
906 }
907
908 #[test]
909 fn test_browser_ios_safari() {
910 assert_eq!(Browser::IosSafari.name(), "iOS Safari");
911 assert_eq!(Browser::IosSafari.engine(), "WebKit");
912 }
913
914 #[test]
915 fn test_browser_chrome_android() {
916 assert_eq!(Browser::ChromeAndroid.name(), "Chrome Android");
917 assert_eq!(Browser::ChromeAndroid.engine(), "Chromium");
918 }
919
920 #[test]
921 fn test_browser_desktop_browsers() {
922 let browsers = Browser::desktop_browsers();
923 assert_eq!(browsers.len(), 3);
924 assert!(browsers.contains(&Browser::Chrome));
925 assert!(browsers.contains(&Browser::Firefox));
926 assert!(browsers.contains(&Browser::Safari));
927 }
928
929 #[test]
930 fn test_browser_mobile_browsers() {
931 let browsers = Browser::mobile_browsers();
932 assert_eq!(browsers.len(), 2);
933 assert!(browsers.contains(&Browser::IosSafari));
934 assert!(browsers.contains(&Browser::ChromeAndroid));
935 }
936
937 #[test]
938 fn test_browser_test_result() {
939 let result = BrowserTestResult {
940 browser: Browser::Chrome,
941 viewport: Viewport::default(),
942 passed: true,
943 duration_ms: 1500,
944 error: None,
945 screenshots: vec!["screenshot1.png".to_string()],
946 };
947 assert!(result.passed);
948 assert!(result.error.is_none());
949 assert_eq!(result.screenshots.len(), 1);
950 }
951
952 #[test]
953 fn test_browser_test_result_failed() {
954 let result = BrowserTestResult {
955 browser: Browser::Firefox,
956 viewport: Viewport {
957 width: 1280,
958 height: 720,
959 device_pixel_ratio: 1.0,
960 },
961 passed: false,
962 duration_ms: 500,
963 error: Some("Element not found".to_string()),
964 screenshots: vec![],
965 };
966 assert!(!result.passed);
967 assert!(result.error.is_some());
968 }
969
970 #[test]
971 fn test_memory_profile_growth_zero_initial() {
972 let profile = MemoryProfile::new("test", 0);
973 assert_eq!(profile.growth_percentage(), 0.0);
974 }
975
976 #[test]
977 fn test_memory_profile_no_growth() {
978 let mut profile = MemoryProfile::new("test", 1000);
979 profile.snapshot(800, 100, Some("shrink".to_string()));
980
981 assert_eq!(profile.current_heap, 800);
982 assert_eq!(profile.peak_heap, 1000);
983 assert!(profile.growth_events.is_empty());
984 }
985
986 #[test]
987 fn test_compare_performance_zero_baseline() {
988 let mut baseline = PerformanceBaseline::new("old");
989 baseline.add_metric("count", 0.0, "n");
990
991 let current = vec![PerformanceMetric {
992 name: "count".to_string(),
993 value: 10.0,
994 unit: "n".to_string(),
995 }];
996
997 let results = compare_performance(&baseline, ¤t, 10.0);
998 assert_eq!(results.len(), 1);
999 assert_eq!(results[0].change_percent, 0.0);
1000 }
1001
1002 #[test]
1003 fn test_compare_performance_no_match() {
1004 let mut baseline = PerformanceBaseline::new("old");
1005 baseline.add_metric("latency", 100.0, "ms");
1006
1007 let current = vec![PerformanceMetric {
1008 name: "throughput".to_string(),
1009 value: 500.0,
1010 unit: "req/s".to_string(),
1011 }];
1012
1013 let results = compare_performance(&baseline, ¤t, 10.0);
1014 assert!(results.is_empty());
1015 }
1016
1017 #[test]
1018 fn test_render_performance_report_negative_change() {
1019 let mut baseline = PerformanceBaseline::new("abc12345");
1020 baseline.add_metric("latency", 100.0, "ms");
1021
1022 let comparisons = vec![PerformanceComparison {
1023 name: "latency".to_string(),
1024 baseline: 100.0,
1025 current: 90.0,
1026 change_percent: -10.0,
1027 status: ComparisonStatus::Ok,
1028 }];
1029
1030 let output = render_performance_report(&baseline, &comparisons);
1031 assert!(output.contains("-10.0%"));
1032 }
1033
1034 #[test]
1035 fn test_render_performance_report_warnings_and_failures() {
1036 let baseline = PerformanceBaseline::new("abc12345");
1037
1038 let comparisons = vec![
1039 PerformanceComparison {
1040 name: "metric1".to_string(),
1041 baseline: 100.0,
1042 current: 109.0,
1043 change_percent: 9.0,
1044 status: ComparisonStatus::Warn,
1045 },
1046 PerformanceComparison {
1047 name: "metric2".to_string(),
1048 baseline: 100.0,
1049 current: 120.0,
1050 change_percent: 20.0,
1051 status: ComparisonStatus::Fail,
1052 },
1053 ];
1054
1055 let output = render_performance_report(&baseline, &comparisons);
1056 assert!(output.contains("1 warnings"));
1057 assert!(output.contains("1 failures"));
1058 }
1059
1060 #[test]
1061 fn test_recording_metadata() {
1062 let mut recording = Recording::new("test", "http://localhost");
1063 recording.metadata = RecordingMetadata {
1064 commit: Some("abc123".to_string()),
1065 test_name: Some("login_test".to_string()),
1066 description: Some("Tests the login flow".to_string()),
1067 };
1068
1069 assert_eq!(recording.metadata.commit, Some("abc123".to_string()));
1070 assert_eq!(recording.metadata.test_name, Some("login_test".to_string()));
1071 }
1072
1073 #[test]
1074 fn test_recording_serde() {
1075 let mut recording = Recording::new("serde_test", "http://localhost");
1076 recording.add_event(RecordedEvent::Click {
1077 x: 10,
1078 y: 20,
1079 selector: None,
1080 timestamp_ms: 0,
1081 });
1082
1083 let json = serde_json::to_string(&recording).unwrap();
1084 let parsed: Recording = serde_json::from_str(&json).unwrap();
1085
1086 assert_eq!(parsed.name, "serde_test");
1087 assert_eq!(parsed.event_count(), 1);
1088 }
1089
1090 #[test]
1091 fn test_performance_baseline_save_load() {
1092 let mut baseline = PerformanceBaseline::new("commit123");
1093 baseline.add_metric("rtf", 1.5, "x");
1094 baseline.add_metric("fps", 60.0, "fps");
1095
1096 let temp_dir = std::env::temp_dir();
1097 let path = temp_dir.join("test_baseline.json");
1098
1099 baseline.save(&path).expect("Failed to save");
1100
1101 let loaded = PerformanceBaseline::load(&path).expect("Failed to load");
1102 assert_eq!(loaded.commit, "commit123");
1103 assert_eq!(loaded.metrics.len(), 2);
1104
1105 let _ = std::fs::remove_file(&path);
1106 }
1107
1108 #[test]
1109 fn test_performance_baseline_load_nonexistent() {
1110 let result = PerformanceBaseline::load(&PathBuf::from("/nonexistent/baseline.json"));
1111 assert!(result.is_err());
1112 }
1113
1114 #[test]
1115 fn test_key_modifiers_all_true() {
1116 let mods = KeyModifiers {
1117 ctrl: true,
1118 alt: true,
1119 shift: true,
1120 meta: true,
1121 };
1122 assert!(mods.ctrl);
1123 assert!(mods.alt);
1124 assert!(mods.shift);
1125 assert!(mods.meta);
1126 }
1127
1128 #[test]
1129 fn test_browser_eq() {
1130 assert_eq!(Browser::Chrome, Browser::Chrome);
1131 assert_ne!(Browser::Chrome, Browser::Firefox);
1132 }
1133
1134 #[test]
1135 fn test_comparison_status_eq() {
1136 assert_eq!(ComparisonStatus::Ok, ComparisonStatus::Ok);
1137 assert_ne!(ComparisonStatus::Ok, ComparisonStatus::Fail);
1138 }
1139}