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_or(0, |d| d.as_millis() as u64),
192            duration_ms: 0,
193            events: Vec::new(),
194            metadata: RecordingMetadata::default(),
195        }
196    }
197
198    /// Add an event to the recording
199    pub fn add_event(&mut self, event: RecordedEvent) {
200        self.events.push(event);
201    }
202
203    /// Get event count
204    pub fn event_count(&self) -> usize {
205        self.events.len()
206    }
207
208    /// Save recording to file
209    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    /// Load recording from file
216    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// =============================================================================
224// E.2 Memory Profiling
225// =============================================================================
226
227/// Memory profile snapshot
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct MemorySnapshot {
230    /// Heap size in bytes
231    pub heap_bytes: u64,
232    /// Timestamp since start (milliseconds)
233    pub timestamp_ms: u64,
234    /// Label for this snapshot
235    pub label: Option<String>,
236}
237
238/// Memory profile for a WASM module
239#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct MemoryProfile {
241    /// Module name
242    pub module_name: String,
243    /// Initial heap size
244    pub initial_heap: u64,
245    /// Peak heap size
246    pub peak_heap: u64,
247    /// Current heap size
248    pub current_heap: u64,
249    /// Memory snapshots over time
250    pub snapshots: Vec<MemorySnapshot>,
251    /// Growth events
252    pub growth_events: Vec<MemoryGrowthEvent>,
253}
254
255/// Memory growth event
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct MemoryGrowthEvent {
258    /// Size before growth
259    pub from_bytes: u64,
260    /// Size after growth
261    pub to_bytes: u64,
262    /// Timestamp (ms)
263    pub timestamp_ms: u64,
264    /// Reason for growth (if known)
265    pub reason: Option<String>,
266}
267
268impl MemoryProfile {
269    /// Create a new memory profile
270    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    /// Record a memory snapshot
286    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    /// Check if memory exceeds threshold
309    pub fn exceeds_threshold(&self, threshold_bytes: u64) -> bool {
310        self.peak_heap > threshold_bytes
311    }
312
313    /// Get memory growth percentage
314    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// =============================================================================
323// E.4 Cross-Browser Testing
324// =============================================================================
325
326/// Supported browser engines
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
328#[serde(rename_all = "lowercase")]
329pub enum Browser {
330    /// Chromium-based (Chrome, Edge, etc.)
331    Chrome,
332    /// Gecko-based (Firefox)
333    Firefox,
334    /// WebKit-based (Safari)
335    Safari,
336    /// iOS Safari
337    IosSafari,
338    /// Chrome Android
339    ChromeAndroid,
340}
341
342impl Browser {
343    /// Get browser display name
344    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    /// Get browser engine
355    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    /// Get all desktop browsers
364    pub fn desktop_browsers() -> Vec<Self> {
365        vec![Self::Chrome, Self::Firefox, Self::Safari]
366    }
367
368    /// Get all mobile browsers
369    pub fn mobile_browsers() -> Vec<Self> {
370        vec![Self::IosSafari, Self::ChromeAndroid]
371    }
372}
373
374/// Cross-browser test configuration
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct BrowserMatrix {
377    /// Browsers to test
378    pub browsers: Vec<Browser>,
379    /// Viewports to test
380    pub viewports: Vec<Viewport>,
381    /// Run in parallel
382    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                }, // Mobile
405            ],
406            parallel: true,
407        }
408    }
409}
410
411/// Cross-browser test result
412#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct BrowserTestResult {
414    /// Browser used
415    pub browser: Browser,
416    /// Viewport used
417    pub viewport: Viewport,
418    /// Whether test passed
419    pub passed: bool,
420    /// Duration in milliseconds
421    pub duration_ms: u64,
422    /// Error message (if failed)
423    pub error: Option<String>,
424    /// Screenshots taken
425    pub screenshots: Vec<String>,
426}
427
428// =============================================================================
429// E.5 Performance Regression Detection
430// =============================================================================
431
432/// Performance metric
433#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct PerformanceMetric {
435    /// Metric name
436    pub name: String,
437    /// Metric value
438    pub value: f64,
439    /// Unit (e.g., "ms", "MB", "x")
440    pub unit: String,
441}
442
443/// Performance baseline
444#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct PerformanceBaseline {
446    /// Baseline version
447    pub version: String,
448    /// Git commit hash
449    pub commit: String,
450    /// Timestamp
451    pub timestamp: u64,
452    /// Metrics
453    pub metrics: Vec<PerformanceMetric>,
454}
455
456impl PerformanceBaseline {
457    /// Create a new baseline
458    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    /// Add a metric
472    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    /// Save baseline to file
481    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    /// Load baseline from file
488    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/// Performance comparison result
496#[derive(Debug, Clone, Serialize, Deserialize)]
497pub struct PerformanceComparison {
498    /// Metric name
499    pub name: String,
500    /// Baseline value
501    pub baseline: f64,
502    /// Current value
503    pub current: f64,
504    /// Change percentage
505    pub change_percent: f64,
506    /// Status (ok, warn, fail)
507    pub status: ComparisonStatus,
508}
509
510/// Comparison status
511#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
512#[serde(rename_all = "lowercase")]
513pub enum ComparisonStatus {
514    /// Within acceptable range
515    Ok,
516    /// Approaching threshold
517    Warn,
518    /// Exceeds threshold
519    Fail,
520}
521
522impl ComparisonStatus {
523    /// Get display symbol
524    pub const fn symbol(&self) -> &'static str {
525        match self {
526            Self::Ok => "✓",
527            Self::Warn => "⚠",
528            Self::Fail => "✗",
529        }
530    }
531}
532
533/// Compare current metrics against baseline
534pub 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
574/// Render performance comparison as text
575pub 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    // Recording tests
639    #[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    // Memory profiling tests
669    #[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    // Browser tests
705    #[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    // Performance tests
728    #[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, &current, 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, &current, 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, &current, 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        // Save
842        recording.save(&path).expect("Failed to save recording");
843
844        // Load
845        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        // Cleanup
850        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    // Additional tests for full coverage
860
861    #[test]
862    fn test_recorded_event_all_variants() {
863        // TextInput
864        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        // NetworkComplete
872        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        // WasmLoaded
881        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        // StateChange
889        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        // Assertion
898        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, &current, 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, &current, 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}