Skip to main content

probador/
wasm_testing.rs

1//! WASM Testing Features Module
2//!
3//! Implements the top 5 WASM/TUI testing features from the spec (Section E):
4//!
5//! 1. **Deterministic Replay** - Record and replay test sessions
6//! 2. **Memory Profiling** - Track WASM linear memory usage
7//! 3. **State Machine Validation** - Playbook integration
8//! 4. **Cross-Browser Testing** - Multi-browser matrix
9//! 5. **Performance Regression** - Baseline tracking
10
11#![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// =============================================================================
28// E.1 Deterministic Replay
29// =============================================================================
30
31/// A recorded event for deterministic replay
32#[derive(Debug, Clone, Serialize, Deserialize)]
33#[serde(tag = "type")]
34pub enum RecordedEvent {
35    /// Mouse click event
36    Click {
37        /// X coordinate
38        x: i32,
39        /// Y coordinate
40        y: i32,
41        /// CSS selector of target element
42        selector: Option<String>,
43        /// Timestamp in milliseconds since recording start
44        timestamp_ms: u64,
45    },
46    /// Keyboard input event
47    KeyPress {
48        /// Key code
49        key: String,
50        /// Modifier keys
51        modifiers: KeyModifiers,
52        /// Timestamp in milliseconds
53        timestamp_ms: u64,
54    },
55    /// Text input event
56    TextInput {
57        /// Input text
58        text: String,
59        /// Target selector
60        selector: Option<String>,
61        /// Timestamp in milliseconds
62        timestamp_ms: u64,
63    },
64    /// Network request completed
65    NetworkComplete {
66        /// Request URL
67        url: String,
68        /// Response status
69        status: u16,
70        /// Duration in milliseconds
71        duration_ms: u64,
72        /// Timestamp
73        timestamp_ms: u64,
74    },
75    /// WASM module loaded
76    WasmLoaded {
77        /// Module URL
78        url: String,
79        /// Module size in bytes
80        size: u64,
81        /// Timestamp
82        timestamp_ms: u64,
83    },
84    /// State transition
85    StateChange {
86        /// Previous state
87        from: String,
88        /// New state
89        to: String,
90        /// Event that triggered the transition
91        event: String,
92        /// Timestamp
93        timestamp_ms: u64,
94    },
95    /// Assertion check
96    Assertion {
97        /// Assertion name
98        name: String,
99        /// Whether assertion passed
100        passed: bool,
101        /// Actual value
102        actual: String,
103        /// Expected value
104        expected: String,
105        /// Timestamp
106        timestamp_ms: u64,
107    },
108}
109
110/// Keyboard modifier keys
111#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112pub struct KeyModifiers {
113    /// Ctrl/Command key
114    pub ctrl: bool,
115    /// Alt key
116    pub alt: bool,
117    /// Shift key
118    pub shift: bool,
119    /// Meta key (Windows/Command)
120    pub meta: bool,
121}
122
123/// A recorded test session
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct Recording {
126    /// Recording version
127    pub version: String,
128    /// Recording name
129    pub name: String,
130    /// URL where recording was made
131    pub url: String,
132    /// Browser user agent
133    pub user_agent: String,
134    /// Viewport dimensions
135    pub viewport: Viewport,
136    /// Start timestamp (Unix milliseconds)
137    pub start_time: u64,
138    /// Total duration in milliseconds
139    pub duration_ms: u64,
140    /// Recorded events
141    pub events: Vec<RecordedEvent>,
142    /// Metadata
143    pub metadata: RecordingMetadata,
144}
145
146/// Viewport dimensions
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct Viewport {
149    /// Width in pixels
150    pub width: u32,
151    /// Height in pixels
152    pub height: u32,
153    /// Device pixel ratio
154    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/// Recording metadata
168#[derive(Debug, Clone, Default, Serialize, Deserialize)]
169pub struct RecordingMetadata {
170    /// Git commit hash
171    pub commit: Option<String>,
172    /// Test name
173    pub test_name: Option<String>,
174    /// Description
175    pub description: Option<String>,
176}
177
178impl Recording {
179    /// Create a new recording
180    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    /// Add an event to the recording
200    pub fn add_event(&mut self, event: RecordedEvent) {
201        self.events.push(event);
202    }
203
204    /// Get event count
205    pub fn event_count(&self) -> usize {
206        self.events.len()
207    }
208
209    /// Save recording to file
210    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    /// Load recording from file
217    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// =============================================================================
225// E.2 Memory Profiling
226// =============================================================================
227
228/// Memory profile snapshot
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct MemorySnapshot {
231    /// Heap size in bytes
232    pub heap_bytes: u64,
233    /// Timestamp since start (milliseconds)
234    pub timestamp_ms: u64,
235    /// Label for this snapshot
236    pub label: Option<String>,
237}
238
239/// Memory profile for a WASM module
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct MemoryProfile {
242    /// Module name
243    pub module_name: String,
244    /// Initial heap size
245    pub initial_heap: u64,
246    /// Peak heap size
247    pub peak_heap: u64,
248    /// Current heap size
249    pub current_heap: u64,
250    /// Memory snapshots over time
251    pub snapshots: Vec<MemorySnapshot>,
252    /// Growth events
253    pub growth_events: Vec<MemoryGrowthEvent>,
254}
255
256/// Memory growth event
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct MemoryGrowthEvent {
259    /// Size before growth
260    pub from_bytes: u64,
261    /// Size after growth
262    pub to_bytes: u64,
263    /// Timestamp (ms)
264    pub timestamp_ms: u64,
265    /// Reason for growth (if known)
266    pub reason: Option<String>,
267}
268
269impl MemoryProfile {
270    /// Create a new memory profile
271    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    /// Record a memory snapshot
287    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    /// Check if memory exceeds threshold
310    pub fn exceeds_threshold(&self, threshold_bytes: u64) -> bool {
311        self.peak_heap > threshold_bytes
312    }
313
314    /// Get memory growth percentage
315    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// =============================================================================
324// E.4 Cross-Browser Testing
325// =============================================================================
326
327/// Supported browser engines
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
329#[serde(rename_all = "lowercase")]
330pub enum Browser {
331    /// Chromium-based (Chrome, Edge, etc.)
332    Chrome,
333    /// Gecko-based (Firefox)
334    Firefox,
335    /// WebKit-based (Safari)
336    Safari,
337    /// iOS Safari
338    IosSafari,
339    /// Chrome Android
340    ChromeAndroid,
341}
342
343impl Browser {
344    /// Get browser display name
345    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    /// Get browser engine
356    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    /// Get all desktop browsers
365    pub fn desktop_browsers() -> Vec<Self> {
366        vec![Self::Chrome, Self::Firefox, Self::Safari]
367    }
368
369    /// Get all mobile browsers
370    pub fn mobile_browsers() -> Vec<Self> {
371        vec![Self::IosSafari, Self::ChromeAndroid]
372    }
373}
374
375/// Cross-browser test configuration
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct BrowserMatrix {
378    /// Browsers to test
379    pub browsers: Vec<Browser>,
380    /// Viewports to test
381    pub viewports: Vec<Viewport>,
382    /// Run in parallel
383    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                }, // Mobile
406            ],
407            parallel: true,
408        }
409    }
410}
411
412/// Cross-browser test result
413#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct BrowserTestResult {
415    /// Browser used
416    pub browser: Browser,
417    /// Viewport used
418    pub viewport: Viewport,
419    /// Whether test passed
420    pub passed: bool,
421    /// Duration in milliseconds
422    pub duration_ms: u64,
423    /// Error message (if failed)
424    pub error: Option<String>,
425    /// Screenshots taken
426    pub screenshots: Vec<String>,
427}
428
429// =============================================================================
430// E.5 Performance Regression Detection
431// =============================================================================
432
433/// Performance metric
434#[derive(Debug, Clone, Serialize, Deserialize)]
435pub struct PerformanceMetric {
436    /// Metric name
437    pub name: String,
438    /// Metric value
439    pub value: f64,
440    /// Unit (e.g., "ms", "MB", "x")
441    pub unit: String,
442}
443
444/// Performance baseline
445#[derive(Debug, Clone, Serialize, Deserialize)]
446pub struct PerformanceBaseline {
447    /// Baseline version
448    pub version: String,
449    /// Git commit hash
450    pub commit: String,
451    /// Timestamp
452    pub timestamp: u64,
453    /// Metrics
454    pub metrics: Vec<PerformanceMetric>,
455}
456
457impl PerformanceBaseline {
458    /// Create a new baseline
459    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    /// Add a metric
474    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    /// Save baseline to file
483    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    /// Load baseline from file
490    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/// Performance comparison result
498#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct PerformanceComparison {
500    /// Metric name
501    pub name: String,
502    /// Baseline value
503    pub baseline: f64,
504    /// Current value
505    pub current: f64,
506    /// Change percentage
507    pub change_percent: f64,
508    /// Status (ok, warn, fail)
509    pub status: ComparisonStatus,
510}
511
512/// Comparison status
513#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
514#[serde(rename_all = "lowercase")]
515pub enum ComparisonStatus {
516    /// Within acceptable range
517    Ok,
518    /// Approaching threshold
519    Warn,
520    /// Exceeds threshold
521    Fail,
522}
523
524impl ComparisonStatus {
525    /// Get display symbol
526    pub const fn symbol(&self) -> &'static str {
527        match self {
528            Self::Ok => "✓",
529            Self::Warn => "⚠",
530            Self::Fail => "✗",
531        }
532    }
533}
534
535/// Compare current metrics against baseline
536pub 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
576/// Render performance comparison as text
577pub 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    // Recording tests
641    #[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    // Memory profiling tests
671    #[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    // Browser tests
707    #[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    // Performance tests
730    #[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, &current, 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, &current, 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, &current, 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        // Save
844        recording.save(&path).expect("Failed to save recording");
845
846        // Load
847        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        // Cleanup
852        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    // Additional tests for full coverage
862
863    #[test]
864    fn test_recorded_event_all_variants() {
865        // TextInput
866        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        // NetworkComplete
874        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        // WasmLoaded
883        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        // StateChange
891        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        // Assertion
900        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, &current, 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, &current, 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}