Skip to main content

jugar_probar/
ux_coverage.rs

1//! UX Coverage Metrics (Feature 24 - EDD Compliance)
2//!
3//! Provides 100% provable UX coverage metrics for WASM games and TUI apps.
4//! Tracks which UI elements, interactions, and states have been tested.
5//!
6//! ## EXTREME TDD: Tests written FIRST per spec
7//!
8//! ## Probar Principles
9//!
10//! - **Error Prevention**: Type-safe coverage tracking prevents blind spots
11//! - **Efficiency**: Efficient hit counting without overhead
12//! - **User Journey Tracking**: Coverage reflects actual user journeys
13//! - **Balanced Testing**: Even distribution of test coverage
14//!
15//! ## Simple Usage
16//!
17//! ```rust
18//! use jugar_probar::gui_coverage;
19//! use jugar_probar::ux_coverage::*;
20//!
21//! // Define your GUI elements once
22//! let mut tracker = gui_coverage! {
23//!     buttons: ["start", "pause", "restart"],
24//!     screens: ["title", "playing", "game_over"]
25//! };
26//!
27//! // Record interactions during tests
28//! tracker.click("start");
29//! tracker.visit("title");
30//!
31//! // Get simple coverage report
32//! println!("{}", tracker.summary()); // "GUI: 33% (2/6 elements)"
33//! ```
34
35use crate::result::{ProbarError, ProbarResult};
36use serde::{Deserialize, Serialize};
37use std::collections::{HashMap, HashSet};
38use std::fmt;
39
40/// A unique identifier for a UI element
41#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
42pub struct ElementId {
43    /// Element type (button, input, label, etc.)
44    pub element_type: String,
45    /// Unique identifier
46    pub id: String,
47    /// Optional parent element ID
48    pub parent: Option<String>,
49}
50
51impl ElementId {
52    /// Create a new element ID
53    #[must_use]
54    pub fn new(element_type: &str, id: &str) -> Self {
55        Self {
56            element_type: element_type.to_string(),
57            id: id.to_string(),
58            parent: None,
59        }
60    }
61
62    /// Create with parent
63    #[must_use]
64    pub fn with_parent(element_type: &str, id: &str, parent: &str) -> Self {
65        Self {
66            element_type: element_type.to_string(),
67            id: id.to_string(),
68            parent: Some(parent.to_string()),
69        }
70    }
71
72    /// Get the full path (parent/id)
73    #[must_use]
74    pub fn full_path(&self) -> String {
75        match &self.parent {
76            Some(parent) => format!("{}/{}", parent, self.id),
77            None => self.id.clone(),
78        }
79    }
80}
81
82impl fmt::Display for ElementId {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(f, "{}:{}", self.element_type, self.full_path())
85    }
86}
87
88/// Types of interactions that can be tracked
89#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
90pub enum InteractionType {
91    /// Element was clicked/tapped
92    Click,
93    /// Element received focus
94    Focus,
95    /// Element lost focus
96    Blur,
97    /// Text was entered
98    Input,
99    /// Element was hovered
100    Hover,
101    /// Element was scrolled
102    Scroll,
103    /// Drag operation started
104    DragStart,
105    /// Drag operation ended
106    DragEnd,
107    /// Key was pressed while element focused
108    KeyPress(String),
109    /// Custom interaction
110    Custom(String),
111}
112
113impl fmt::Display for InteractionType {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        match self {
116            Self::Click => write!(f, "click"),
117            Self::Focus => write!(f, "focus"),
118            Self::Blur => write!(f, "blur"),
119            Self::Input => write!(f, "input"),
120            Self::Hover => write!(f, "hover"),
121            Self::Scroll => write!(f, "scroll"),
122            Self::DragStart => write!(f, "drag_start"),
123            Self::DragEnd => write!(f, "drag_end"),
124            Self::KeyPress(key) => write!(f, "keypress:{key}"),
125            Self::Custom(name) => write!(f, "custom:{name}"),
126        }
127    }
128}
129
130/// Tracked interaction on an element
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct TrackedInteraction {
133    /// Element that was interacted with
134    pub element: ElementId,
135    /// Type of interaction
136    pub interaction: InteractionType,
137    /// Number of times this interaction occurred
138    pub count: u64,
139}
140
141/// UI state that can be tracked
142#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
143pub struct StateId {
144    /// State category (screen, modal, menu, etc.)
145    pub category: String,
146    /// State name
147    pub name: String,
148}
149
150impl StateId {
151    /// Create a new state ID
152    #[must_use]
153    pub fn new(category: &str, name: &str) -> Self {
154        Self {
155            category: category.to_string(),
156            name: name.to_string(),
157        }
158    }
159}
160
161impl fmt::Display for StateId {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        write!(f, "{}:{}", self.category, self.name)
164    }
165}
166
167/// Coverage report for a single element
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct ElementCoverage {
170    /// Element ID
171    pub element: ElementId,
172    /// Interactions that have been tested
173    pub tested_interactions: HashSet<InteractionType>,
174    /// Expected interactions for full coverage
175    pub expected_interactions: HashSet<InteractionType>,
176    /// Whether element was visible during tests
177    pub was_visible: bool,
178    /// Whether element was reachable/navigable
179    pub was_reachable: bool,
180}
181
182impl ElementCoverage {
183    /// Create a new element coverage tracker
184    #[must_use]
185    pub fn new(element: ElementId) -> Self {
186        Self {
187            element,
188            tested_interactions: HashSet::new(),
189            expected_interactions: HashSet::new(),
190            was_visible: false,
191            was_reachable: false,
192        }
193    }
194
195    /// Add an expected interaction
196    pub fn expect(&mut self, interaction: InteractionType) {
197        self.expected_interactions.insert(interaction);
198    }
199
200    /// Record a tested interaction
201    pub fn record(&mut self, interaction: InteractionType) {
202        self.tested_interactions.insert(interaction);
203    }
204
205    /// Mark as visible
206    pub fn mark_visible(&mut self) {
207        self.was_visible = true;
208    }
209
210    /// Mark as reachable
211    pub fn mark_reachable(&mut self) {
212        self.was_reachable = true;
213    }
214
215    /// Get coverage percentage (0.0 to 1.0)
216    #[must_use]
217    pub fn coverage_ratio(&self) -> f64 {
218        if self.expected_interactions.is_empty() {
219            return 1.0;
220        }
221        let covered = self
222            .tested_interactions
223            .intersection(&self.expected_interactions)
224            .count();
225        covered as f64 / self.expected_interactions.len() as f64
226    }
227
228    /// Check if fully covered
229    #[must_use]
230    pub fn is_fully_covered(&self) -> bool {
231        self.expected_interactions
232            .iter()
233            .all(|i| self.tested_interactions.contains(i))
234    }
235
236    /// Get uncovered interactions
237    #[must_use]
238    pub fn uncovered(&self) -> Vec<&InteractionType> {
239        self.expected_interactions
240            .iter()
241            .filter(|i| !self.tested_interactions.contains(i))
242            .collect()
243    }
244}
245
246/// UX Coverage Tracker
247#[derive(Debug, Default)]
248pub struct UxCoverageTracker {
249    /// Coverage by element
250    elements: HashMap<String, ElementCoverage>,
251    /// States that have been visited
252    visited_states: HashSet<StateId>,
253    /// Expected states for full coverage
254    expected_states: HashSet<StateId>,
255    /// Interaction counts
256    interaction_counts: HashMap<String, u64>,
257    /// User journeys (sequences of states)
258    journeys: Vec<Vec<StateId>>,
259    /// Current journey being recorded
260    current_journey: Vec<StateId>,
261}
262
263impl UxCoverageTracker {
264    /// Create a new UX coverage tracker
265    #[must_use]
266    pub fn new() -> Self {
267        Self::default()
268    }
269
270    /// Register an element with expected interactions
271    pub fn register_element(&mut self, element: ElementId, expected: &[InteractionType]) {
272        let key = element.to_string();
273        let mut coverage = ElementCoverage::new(element);
274        for interaction in expected {
275            coverage.expect(interaction.clone());
276        }
277        self.elements.insert(key, coverage);
278    }
279
280    /// Register a button element (click expected)
281    pub fn register_button(&mut self, id: &str) {
282        let element = ElementId::new("button", id);
283        self.register_element(element, &[InteractionType::Click]);
284    }
285
286    /// Register an input element (focus, input, blur expected)
287    pub fn register_input(&mut self, id: &str) {
288        let element = ElementId::new("input", id);
289        self.register_element(
290            element,
291            &[
292                InteractionType::Focus,
293                InteractionType::Input,
294                InteractionType::Blur,
295            ],
296        );
297    }
298
299    /// Register a clickable element
300    pub fn register_clickable(&mut self, element_type: &str, id: &str) {
301        let element = ElementId::new(element_type, id);
302        self.register_element(element, &[InteractionType::Click]);
303    }
304
305    /// Register an expected state
306    pub fn register_state(&mut self, state: StateId) {
307        self.expected_states.insert(state);
308    }
309
310    /// Register a screen state
311    pub fn register_screen(&mut self, name: &str) {
312        self.register_state(StateId::new("screen", name));
313    }
314
315    /// Register a modal state
316    pub fn register_modal(&mut self, name: &str) {
317        self.register_state(StateId::new("modal", name));
318    }
319
320    /// Record an interaction
321    pub fn record_interaction(&mut self, element: &ElementId, interaction: InteractionType) {
322        let key = element.to_string();
323
324        if let Some(coverage) = self.elements.get_mut(&key) {
325            coverage.record(interaction.clone());
326        }
327
328        // Update interaction counts
329        let count_key = format!("{}:{}", key, interaction);
330        *self.interaction_counts.entry(count_key).or_insert(0) += 1;
331    }
332
333    /// Record element visibility
334    pub fn record_visibility(&mut self, element: &ElementId) {
335        let key = element.to_string();
336        if let Some(coverage) = self.elements.get_mut(&key) {
337            coverage.mark_visible();
338        }
339    }
340
341    /// Record element reachability
342    pub fn record_reachability(&mut self, element: &ElementId) {
343        let key = element.to_string();
344        if let Some(coverage) = self.elements.get_mut(&key) {
345            coverage.mark_reachable();
346        }
347    }
348
349    /// Record a state visit
350    pub fn record_state(&mut self, state: StateId) {
351        self.visited_states.insert(state.clone());
352        self.current_journey.push(state);
353    }
354
355    /// End current journey and start a new one
356    pub fn end_journey(&mut self) {
357        if !self.current_journey.is_empty() {
358            self.journeys
359                .push(std::mem::take(&mut self.current_journey));
360        }
361    }
362
363    /// Get overall element coverage percentage
364    #[must_use]
365    pub fn element_coverage(&self) -> f64 {
366        if self.elements.is_empty() {
367            return 1.0;
368        }
369        let total_coverage: f64 = self
370            .elements
371            .values()
372            .map(ElementCoverage::coverage_ratio)
373            .sum();
374        total_coverage / self.elements.len() as f64
375    }
376
377    /// Get state coverage percentage
378    #[must_use]
379    pub fn state_coverage(&self) -> f64 {
380        if self.expected_states.is_empty() {
381            return 1.0;
382        }
383        let visited = self
384            .expected_states
385            .iter()
386            .filter(|s| self.visited_states.contains(s))
387            .count();
388        visited as f64 / self.expected_states.len() as f64
389    }
390
391    /// Get overall UX coverage percentage
392    #[must_use]
393    pub fn overall_coverage(&self) -> f64 {
394        let element = self.element_coverage();
395        let state = self.state_coverage();
396
397        // Weight equally if both have expectations
398        if self.elements.is_empty() {
399            return state;
400        }
401        if self.expected_states.is_empty() {
402            return element;
403        }
404
405        (element + state) / 2.0
406    }
407
408    /// Check if 100% coverage achieved
409    #[must_use]
410    pub fn is_complete(&self) -> bool {
411        (self.overall_coverage() - 1.0).abs() < f64::EPSILON
412    }
413
414    /// Get uncovered elements
415    #[must_use]
416    pub fn uncovered_elements(&self) -> Vec<&ElementCoverage> {
417        self.elements
418            .values()
419            .filter(|e| !e.is_fully_covered())
420            .collect()
421    }
422
423    /// Get unvisited states
424    #[must_use]
425    pub fn unvisited_states(&self) -> Vec<&StateId> {
426        self.expected_states
427            .iter()
428            .filter(|s| !self.visited_states.contains(s))
429            .collect()
430    }
431
432    /// Get all recorded journeys
433    #[must_use]
434    pub fn journeys(&self) -> &[Vec<StateId>] {
435        &self.journeys
436    }
437
438    /// Generate a coverage report
439    #[must_use]
440    pub fn generate_report(&self) -> UxCoverageReport {
441        UxCoverageReport {
442            overall_coverage: self.overall_coverage(),
443            element_coverage: self.element_coverage(),
444            state_coverage: self.state_coverage(),
445            total_elements: self.elements.len(),
446            covered_elements: self
447                .elements
448                .values()
449                .filter(|e| e.is_fully_covered())
450                .count(),
451            total_states: self.expected_states.len(),
452            covered_states: self.visited_states.len(),
453            total_interactions: self.interaction_counts.values().sum(),
454            unique_journeys: self.journeys.len(),
455            is_complete: self.is_complete(),
456        }
457    }
458
459    /// Assert minimum coverage
460    pub fn assert_coverage(&self, min_coverage: f64) -> ProbarResult<()> {
461        let actual = self.overall_coverage();
462        if actual >= min_coverage {
463            Ok(())
464        } else {
465            let uncovered_elements: Vec<String> = self
466                .uncovered_elements()
467                .iter()
468                .map(|e| e.element.to_string())
469                .collect();
470            let unvisited_states: Vec<String> = self
471                .unvisited_states()
472                .iter()
473                .map(|s| s.to_string())
474                .collect();
475
476            Err(ProbarError::AssertionFailed {
477                message: format!(
478                    "UX coverage {:.1}% is below minimum {:.1}%\n\
479                    Uncovered elements: {:?}\n\
480                    Unvisited states: {:?}",
481                    actual * 100.0,
482                    min_coverage * 100.0,
483                    uncovered_elements,
484                    unvisited_states
485                ),
486            })
487        }
488    }
489
490    /// Assert 100% coverage
491    pub fn assert_complete(&self) -> ProbarResult<()> {
492        self.assert_coverage(1.0)
493    }
494
495    // =========================================================================
496    // SIMPLE CONVENIENCE API - Trivial GUI coverage tracking
497    // =========================================================================
498
499    /// Simple click recording - just pass the button ID
500    ///
501    /// # Example
502    /// ```rust
503    /// # use jugar_probar::ux_coverage::UxCoverageTracker;
504    /// let mut tracker = UxCoverageTracker::new();
505    /// tracker.register_button("submit");
506    /// tracker.click("submit");
507    /// assert!(tracker.is_complete());
508    /// ```
509    pub fn click(&mut self, id: &str) {
510        let element = ElementId::new("button", id);
511        self.record_interaction(&element, InteractionType::Click);
512    }
513
514    /// Simple input recording - records focus, input, and blur
515    pub fn input(&mut self, id: &str) {
516        let element = ElementId::new("input", id);
517        self.record_interaction(&element, InteractionType::Focus);
518        self.record_interaction(&element, InteractionType::Input);
519        self.record_interaction(&element, InteractionType::Blur);
520    }
521
522    /// Simple state/screen visit recording
523    pub fn visit(&mut self, screen: &str) {
524        self.record_state(StateId::new("screen", screen));
525    }
526
527    /// Simple modal visit recording
528    pub fn visit_modal(&mut self, modal: &str) {
529        self.record_state(StateId::new("modal", modal));
530    }
531
532    /// Get a simple one-line summary
533    ///
534    /// Returns: `"GUI: 85% (17/20 elements, 4/5 screens)"`
535    #[must_use]
536    pub fn summary(&self) -> String {
537        let report = self.generate_report();
538        if report.total_states == 0 {
539            format!(
540                "GUI: {:.0}% ({}/{} elements)",
541                report.element_coverage * 100.0,
542                report.covered_elements,
543                report.total_elements
544            )
545        } else if report.total_elements == 0 {
546            format!(
547                "GUI: {:.0}% ({}/{} screens)",
548                report.state_coverage * 100.0,
549                report.covered_states,
550                report.total_states
551            )
552        } else {
553            format!(
554                "GUI: {:.0}% ({}/{} elements, {}/{} screens)",
555                report.overall_coverage * 100.0,
556                report.covered_elements,
557                report.total_elements,
558                report.covered_states,
559                report.total_states
560            )
561        }
562    }
563
564    /// Get coverage as a simple percentage (0-100)
565    #[must_use]
566    pub fn percent(&self) -> f64 {
567        self.overall_coverage() * 100.0
568    }
569
570    /// Check if coverage meets a threshold (as percentage 0-100)
571    #[must_use]
572    pub fn meets(&self, threshold_percent: f64) -> bool {
573        self.percent() >= threshold_percent
574    }
575}
576
577/// UX Coverage Report
578#[derive(Debug, Clone, Serialize, Deserialize)]
579pub struct UxCoverageReport {
580    /// Overall UX coverage percentage (0.0 to 1.0)
581    pub overall_coverage: f64,
582    /// Element interaction coverage
583    pub element_coverage: f64,
584    /// State/screen coverage
585    pub state_coverage: f64,
586    /// Total elements registered
587    pub total_elements: usize,
588    /// Elements fully covered
589    pub covered_elements: usize,
590    /// Total states expected
591    pub total_states: usize,
592    /// States visited
593    pub covered_states: usize,
594    /// Total interactions recorded
595    pub total_interactions: u64,
596    /// Number of unique user journeys
597    pub unique_journeys: usize,
598    /// Whether 100% coverage achieved
599    pub is_complete: bool,
600}
601
602impl UxCoverageReport {
603    /// Format as text summary
604    #[must_use]
605    pub fn summary(&self) -> String {
606        format!(
607            "UX Coverage Report\n\
608            ==================\n\
609            Overall Coverage: {:.1}%\n\
610            Element Coverage: {:.1}% ({}/{} elements)\n\
611            State Coverage:   {:.1}% ({}/{} states)\n\
612            Interactions:     {}\n\
613            User Journeys:    {}\n\
614            Status:           {}",
615            self.overall_coverage * 100.0,
616            self.element_coverage * 100.0,
617            self.covered_elements,
618            self.total_elements,
619            self.state_coverage * 100.0,
620            self.covered_states,
621            self.total_states,
622            self.total_interactions,
623            self.unique_journeys,
624            if self.is_complete {
625                "COMPLETE"
626            } else {
627                "INCOMPLETE"
628            }
629        )
630    }
631}
632
633impl fmt::Display for UxCoverageReport {
634    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
635        write!(f, "{}", self.summary())
636    }
637}
638
639/// Builder for defining UX coverage requirements
640#[derive(Debug, Default)]
641pub struct UxCoverageBuilder {
642    tracker: UxCoverageTracker,
643}
644
645impl UxCoverageBuilder {
646    /// Create a new builder
647    #[must_use]
648    pub fn new() -> Self {
649        Self::default()
650    }
651
652    /// Add a button
653    #[must_use]
654    pub fn button(mut self, id: &str) -> Self {
655        self.tracker.register_button(id);
656        self
657    }
658
659    /// Add an input field
660    #[must_use]
661    pub fn input(mut self, id: &str) -> Self {
662        self.tracker.register_input(id);
663        self
664    }
665
666    /// Add a clickable element
667    #[must_use]
668    pub fn clickable(mut self, element_type: &str, id: &str) -> Self {
669        self.tracker.register_clickable(element_type, id);
670        self
671    }
672
673    /// Add a screen state
674    #[must_use]
675    pub fn screen(mut self, name: &str) -> Self {
676        self.tracker.register_screen(name);
677        self
678    }
679
680    /// Add a modal state
681    #[must_use]
682    pub fn modal(mut self, name: &str) -> Self {
683        self.tracker.register_modal(name);
684        self
685    }
686
687    /// Add a custom element with expected interactions
688    #[must_use]
689    pub fn element(mut self, element: ElementId, expected: &[InteractionType]) -> Self {
690        self.tracker.register_element(element, expected);
691        self
692    }
693
694    /// Add a custom state
695    #[must_use]
696    pub fn state(mut self, category: &str, name: &str) -> Self {
697        self.tracker.register_state(StateId::new(category, name));
698        self
699    }
700
701    /// Build the tracker
702    #[must_use]
703    pub fn build(self) -> UxCoverageTracker {
704        self.tracker
705    }
706}
707
708// =============================================================================
709// MACRO: gui_coverage! - The simplest way to define GUI coverage requirements
710// =============================================================================
711
712/// Create a GUI coverage tracker with minimal boilerplate
713///
714/// # Example
715///
716/// ```rust
717/// use jugar_probar::gui_coverage;
718///
719/// // Define what needs to be tested
720/// let mut gui = gui_coverage! {
721///     buttons: ["start", "pause", "quit"],
722///     inputs: ["username", "password"],
723///     screens: ["login", "main", "settings"]
724/// };
725///
726/// // During tests, record interactions
727/// gui.click("start");
728/// gui.input("username");
729/// gui.visit("login");
730///
731/// // Check coverage
732/// println!("{}", gui.summary());  // "GUI: 33% (3/9 elements, 1/3 screens)"
733/// assert!(gui.meets(30.0));       // At least 30% covered
734/// ```
735#[macro_export]
736macro_rules! gui_coverage {
737    // Full syntax with all options
738    {
739        $(buttons: [$($btn:expr),* $(,)?])?
740        $(, inputs: [$($inp:expr),* $(,)?])?
741        $(, screens: [$($scr:expr),* $(,)?])?
742        $(, modals: [$($mod:expr),* $(,)?])?
743        $(,)?
744    } => {{
745        let mut builder = $crate::ux_coverage::UxCoverageBuilder::new();
746        $($(
747            builder = builder.button($btn);
748        )*)?
749        $($(
750            builder = builder.input($inp);
751        )*)?
752        $($(
753            builder = builder.screen($scr);
754        )*)?
755        $($(
756            builder = builder.modal($mod);
757        )*)?
758        builder.build()
759    }};
760}
761
762/// Shorthand for a calculator-style GUI (common pattern)
763///
764/// Creates a tracker with:
765/// - Digit buttons (0-9)
766/// - Operator buttons (+, -, *, /, =, C)
767/// - Display and input screens
768#[must_use]
769pub fn calculator_coverage() -> UxCoverageTracker {
770    UxCoverageBuilder::new()
771        // Digit buttons
772        .button("btn-0")
773        .button("btn-1")
774        .button("btn-2")
775        .button("btn-3")
776        .button("btn-4")
777        .button("btn-5")
778        .button("btn-6")
779        .button("btn-7")
780        .button("btn-8")
781        .button("btn-9")
782        // Operator buttons
783        .button("btn-plus")
784        .button("btn-minus")
785        .button("btn-times")
786        .button("btn-divide")
787        .button("btn-equals")
788        .button("btn-clear")
789        .button("btn-decimal")
790        .button("btn-power")
791        .button("btn-open-paren")
792        .button("btn-close-paren")
793        // Screens
794        .screen("calculator")
795        .screen("history")
796        .build()
797}
798
799/// Shorthand for a simple game GUI
800#[must_use]
801pub fn game_coverage(buttons: &[&str], screens: &[&str]) -> UxCoverageTracker {
802    let mut builder = UxCoverageBuilder::new();
803    for btn in buttons {
804        builder = builder.button(btn);
805    }
806    for screen in screens {
807        builder = builder.screen(screen);
808    }
809    builder.build()
810}
811
812#[cfg(test)]
813mod tests {
814    use super::*;
815
816    mod element_id_tests {
817        use super::*;
818
819        #[test]
820        fn test_new() {
821            let id = ElementId::new("button", "submit");
822            assert_eq!(id.element_type, "button");
823            assert_eq!(id.id, "submit");
824            assert!(id.parent.is_none());
825        }
826
827        #[test]
828        fn test_with_parent() {
829            let id = ElementId::with_parent("button", "ok", "dialog");
830            assert_eq!(id.parent, Some("dialog".to_string()));
831        }
832
833        #[test]
834        fn test_full_path() {
835            let id1 = ElementId::new("button", "submit");
836            assert_eq!(id1.full_path(), "submit");
837
838            let id2 = ElementId::with_parent("button", "ok", "dialog");
839            assert_eq!(id2.full_path(), "dialog/ok");
840        }
841
842        #[test]
843        fn test_display() {
844            let id = ElementId::new("button", "submit");
845            assert_eq!(format!("{}", id), "button:submit");
846        }
847    }
848
849    mod interaction_type_tests {
850        use super::*;
851
852        #[test]
853        fn test_display() {
854            assert_eq!(format!("{}", InteractionType::Click), "click");
855            assert_eq!(format!("{}", InteractionType::Focus), "focus");
856            assert_eq!(
857                format!("{}", InteractionType::KeyPress("Enter".to_string())),
858                "keypress:Enter"
859            );
860            assert_eq!(
861                format!("{}", InteractionType::Custom("swipe".to_string())),
862                "custom:swipe"
863            );
864        }
865    }
866
867    mod element_coverage_tests {
868        use super::*;
869
870        #[test]
871        fn test_new() {
872            let element = ElementId::new("button", "test");
873            let coverage = ElementCoverage::new(element);
874
875            assert!(coverage.tested_interactions.is_empty());
876            assert!(coverage.expected_interactions.is_empty());
877        }
878
879        #[test]
880        fn test_coverage_ratio() {
881            let element = ElementId::new("button", "test");
882            let mut coverage = ElementCoverage::new(element);
883
884            coverage.expect(InteractionType::Click);
885            coverage.expect(InteractionType::Hover);
886
887            assert!((coverage.coverage_ratio() - 0.0).abs() < f64::EPSILON);
888
889            coverage.record(InteractionType::Click);
890            assert!((coverage.coverage_ratio() - 0.5).abs() < f64::EPSILON);
891
892            coverage.record(InteractionType::Hover);
893            assert!((coverage.coverage_ratio() - 1.0).abs() < f64::EPSILON);
894        }
895
896        #[test]
897        fn test_is_fully_covered() {
898            let element = ElementId::new("button", "test");
899            let mut coverage = ElementCoverage::new(element);
900
901            coverage.expect(InteractionType::Click);
902            assert!(!coverage.is_fully_covered());
903
904            coverage.record(InteractionType::Click);
905            assert!(coverage.is_fully_covered());
906        }
907
908        #[test]
909        fn test_uncovered() {
910            let element = ElementId::new("button", "test");
911            let mut coverage = ElementCoverage::new(element);
912
913            coverage.expect(InteractionType::Click);
914            coverage.expect(InteractionType::Hover);
915            coverage.record(InteractionType::Click);
916
917            let uncovered = coverage.uncovered();
918            assert_eq!(uncovered.len(), 1);
919            assert_eq!(uncovered[0], &InteractionType::Hover);
920        }
921    }
922
923    mod ux_coverage_tracker_tests {
924        use super::*;
925
926        #[test]
927        fn test_new() {
928            let tracker = UxCoverageTracker::new();
929            assert!(tracker.is_complete()); // Empty tracker is "complete"
930        }
931
932        #[test]
933        fn test_register_button() {
934            let mut tracker = UxCoverageTracker::new();
935            tracker.register_button("submit");
936
937            assert_eq!(tracker.elements.len(), 1);
938            assert!((tracker.element_coverage() - 0.0).abs() < f64::EPSILON);
939        }
940
941        #[test]
942        fn test_record_interaction() {
943            let mut tracker = UxCoverageTracker::new();
944            tracker.register_button("submit");
945
946            let element = ElementId::new("button", "submit");
947            tracker.record_interaction(&element, InteractionType::Click);
948
949            assert!((tracker.element_coverage() - 1.0).abs() < f64::EPSILON);
950        }
951
952        #[test]
953        fn test_register_state() {
954            let mut tracker = UxCoverageTracker::new();
955            tracker.register_screen("home");
956            tracker.register_screen("settings");
957
958            assert_eq!(tracker.expected_states.len(), 2);
959            assert!((tracker.state_coverage() - 0.0).abs() < f64::EPSILON);
960        }
961
962        #[test]
963        fn test_record_state() {
964            let mut tracker = UxCoverageTracker::new();
965            tracker.register_screen("home");
966            tracker.register_screen("settings");
967
968            tracker.record_state(StateId::new("screen", "home"));
969            assert!((tracker.state_coverage() - 0.5).abs() < f64::EPSILON);
970
971            tracker.record_state(StateId::new("screen", "settings"));
972            assert!((tracker.state_coverage() - 1.0).abs() < f64::EPSILON);
973        }
974
975        #[test]
976        fn test_overall_coverage() {
977            let mut tracker = UxCoverageTracker::new();
978
979            // Register 2 buttons and 2 screens
980            tracker.register_button("btn1");
981            tracker.register_button("btn2");
982            tracker.register_screen("home");
983            tracker.register_screen("settings");
984
985            // Cover 1 button and 1 screen
986            tracker.record_interaction(&ElementId::new("button", "btn1"), InteractionType::Click);
987            tracker.record_state(StateId::new("screen", "home"));
988
989            // 50% element + 50% state = 50% overall
990            assert!((tracker.overall_coverage() - 0.5).abs() < f64::EPSILON);
991        }
992
993        #[test]
994        fn test_journeys() {
995            let mut tracker = UxCoverageTracker::new();
996
997            tracker.record_state(StateId::new("screen", "home"));
998            tracker.record_state(StateId::new("screen", "settings"));
999            tracker.end_journey();
1000
1001            tracker.record_state(StateId::new("screen", "home"));
1002            tracker.record_state(StateId::new("screen", "profile"));
1003            tracker.end_journey();
1004
1005            assert_eq!(tracker.journeys().len(), 2);
1006        }
1007
1008        #[test]
1009        fn test_assert_coverage_pass() {
1010            let mut tracker = UxCoverageTracker::new();
1011            tracker.register_button("btn");
1012            tracker.record_interaction(&ElementId::new("button", "btn"), InteractionType::Click);
1013
1014            assert!(tracker.assert_coverage(1.0).is_ok());
1015        }
1016
1017        #[test]
1018        fn test_assert_coverage_fail() {
1019            let mut tracker = UxCoverageTracker::new();
1020            tracker.register_button("btn");
1021
1022            assert!(tracker.assert_coverage(1.0).is_err());
1023        }
1024
1025        #[test]
1026        fn test_uncovered_elements() {
1027            let mut tracker = UxCoverageTracker::new();
1028            tracker.register_button("btn1");
1029            tracker.register_button("btn2");
1030            tracker.record_interaction(&ElementId::new("button", "btn1"), InteractionType::Click);
1031
1032            let uncovered = tracker.uncovered_elements();
1033            assert_eq!(uncovered.len(), 1);
1034        }
1035
1036        #[test]
1037        fn test_unvisited_states() {
1038            let mut tracker = UxCoverageTracker::new();
1039            tracker.register_screen("home");
1040            tracker.register_screen("settings");
1041            tracker.record_state(StateId::new("screen", "home"));
1042
1043            let unvisited = tracker.unvisited_states();
1044            assert_eq!(unvisited.len(), 1);
1045        }
1046    }
1047
1048    mod ux_coverage_report_tests {
1049        use super::*;
1050
1051        #[test]
1052        fn test_generate_report() {
1053            let mut tracker = UxCoverageTracker::new();
1054            tracker.register_button("btn1");
1055            tracker.register_button("btn2");
1056            tracker.register_screen("home");
1057
1058            tracker.record_interaction(&ElementId::new("button", "btn1"), InteractionType::Click);
1059            tracker.record_state(StateId::new("screen", "home"));
1060
1061            let report = tracker.generate_report();
1062            assert_eq!(report.total_elements, 2);
1063            assert_eq!(report.covered_elements, 1);
1064            assert_eq!(report.total_states, 1);
1065            assert_eq!(report.covered_states, 1);
1066            assert!(!report.is_complete);
1067        }
1068
1069        #[test]
1070        fn test_complete_report() {
1071            let mut tracker = UxCoverageTracker::new();
1072            tracker.register_button("btn");
1073            tracker.record_interaction(&ElementId::new("button", "btn"), InteractionType::Click);
1074
1075            let report = tracker.generate_report();
1076            assert!(report.is_complete);
1077        }
1078    }
1079
1080    mod ux_coverage_builder_tests {
1081        use super::*;
1082
1083        #[test]
1084        fn test_builder() {
1085            let tracker = UxCoverageBuilder::new()
1086                .button("submit")
1087                .button("cancel")
1088                .input("username")
1089                .screen("login")
1090                .screen("dashboard")
1091                .build();
1092
1093            assert_eq!(tracker.elements.len(), 3);
1094            assert_eq!(tracker.expected_states.len(), 2);
1095        }
1096
1097        #[test]
1098        fn test_custom_element() {
1099            let tracker = UxCoverageBuilder::new()
1100                .element(
1101                    ElementId::new("canvas", "game"),
1102                    &[InteractionType::Click, InteractionType::Hover],
1103                )
1104                .build();
1105
1106            assert_eq!(tracker.elements.len(), 1);
1107        }
1108    }
1109
1110    mod additional_tracker_tests {
1111        use super::*;
1112
1113        #[test]
1114        fn test_register_input() {
1115            let mut tracker = UxCoverageTracker::new();
1116            tracker.register_input("username");
1117
1118            // Input should expect focus, input, blur
1119            assert_eq!(tracker.elements.len(), 1);
1120        }
1121
1122        #[test]
1123        fn test_register_clickable() {
1124            let mut tracker = UxCoverageTracker::new();
1125            tracker.register_clickable("link", "home");
1126
1127            assert_eq!(tracker.elements.len(), 1);
1128        }
1129
1130        #[test]
1131        fn test_register_modal() {
1132            let mut tracker = UxCoverageTracker::new();
1133            tracker.register_modal("confirm_dialog");
1134
1135            assert!(tracker
1136                .expected_states
1137                .contains(&StateId::new("modal", "confirm_dialog")));
1138        }
1139
1140        #[test]
1141        fn test_mark_visible_reachable() {
1142            let element = ElementId::new("button", "test");
1143            let mut coverage = ElementCoverage::new(element);
1144
1145            assert!(!coverage.was_visible);
1146            assert!(!coverage.was_reachable);
1147
1148            coverage.mark_visible();
1149            assert!(coverage.was_visible);
1150
1151            coverage.mark_reachable();
1152            assert!(coverage.was_reachable);
1153        }
1154
1155        #[test]
1156        fn test_tracker_debug() {
1157            let tracker = UxCoverageTracker::new();
1158            let debug = format!("{:?}", tracker);
1159            assert!(debug.contains("UxCoverageTracker"));
1160        }
1161    }
1162
1163    mod interaction_type_display_tests {
1164        use super::*;
1165
1166        #[test]
1167        fn test_all_interaction_displays() {
1168            assert_eq!(format!("{}", InteractionType::Click), "click");
1169            assert_eq!(format!("{}", InteractionType::Focus), "focus");
1170            assert_eq!(format!("{}", InteractionType::Blur), "blur");
1171            assert_eq!(format!("{}", InteractionType::Input), "input");
1172            assert_eq!(format!("{}", InteractionType::Hover), "hover");
1173            assert_eq!(format!("{}", InteractionType::Scroll), "scroll");
1174            assert_eq!(format!("{}", InteractionType::DragStart), "drag_start");
1175            assert_eq!(format!("{}", InteractionType::DragEnd), "drag_end");
1176            assert_eq!(
1177                format!("{}", InteractionType::KeyPress("Enter".to_string())),
1178                "keypress:Enter"
1179            );
1180            assert_eq!(
1181                format!("{}", InteractionType::Custom("swipe".to_string())),
1182                "custom:swipe"
1183            );
1184        }
1185    }
1186
1187    mod state_id_tests {
1188        use super::*;
1189
1190        #[test]
1191        fn test_display() {
1192            let state = StateId::new("screen", "home");
1193            assert_eq!(format!("{}", state), "screen:home");
1194        }
1195
1196        #[test]
1197        fn test_equality() {
1198            let state1 = StateId::new("screen", "home");
1199            let state2 = StateId::new("screen", "home");
1200            let state3 = StateId::new("screen", "settings");
1201
1202            assert_eq!(state1, state2);
1203            assert_ne!(state1, state3);
1204        }
1205    }
1206
1207    mod element_coverage_additional_tests {
1208        use super::*;
1209
1210        #[test]
1211        fn test_coverage_ratio_empty() {
1212            let element = ElementId::new("button", "test");
1213            let coverage = ElementCoverage::new(element);
1214
1215            // Empty expected means 100% coverage
1216            assert!((coverage.coverage_ratio() - 1.0).abs() < f64::EPSILON);
1217        }
1218
1219        #[test]
1220        fn test_is_fully_covered_empty() {
1221            let element = ElementId::new("button", "test");
1222            let coverage = ElementCoverage::new(element);
1223
1224            assert!(coverage.is_fully_covered());
1225        }
1226
1227        #[test]
1228        fn test_debug() {
1229            let element = ElementId::new("button", "test");
1230            let coverage = ElementCoverage::new(element);
1231            let debug = format!("{:?}", coverage);
1232            assert!(debug.contains("ElementCoverage"));
1233        }
1234    }
1235
1236    mod tracked_interaction_tests {
1237        use super::*;
1238
1239        #[test]
1240        fn test_tracked_interaction() {
1241            let interaction = TrackedInteraction {
1242                element: ElementId::new("button", "submit"),
1243                interaction: InteractionType::Click,
1244                count: 5,
1245            };
1246
1247            assert_eq!(interaction.count, 5);
1248            let debug = format!("{:?}", interaction);
1249            assert!(debug.contains("TrackedInteraction"));
1250        }
1251    }
1252
1253    mod report_tests {
1254        use super::*;
1255
1256        #[test]
1257        fn test_report_debug() {
1258            let tracker = UxCoverageTracker::new();
1259            let report = tracker.generate_report();
1260            let debug = format!("{:?}", report);
1261            assert!(debug.contains("UxCoverageReport"));
1262        }
1263    }
1264
1265    mod pong_game_coverage_tests {
1266        use super::*;
1267
1268        #[test]
1269        fn test_pong_full_coverage() {
1270            // Define expected coverage for a Pong game
1271            let mut tracker = UxCoverageBuilder::new()
1272                .button("start_game")
1273                .button("pause")
1274                .button("restart")
1275                .clickable("paddle", "player")
1276                .screen("title")
1277                .screen("playing")
1278                .screen("paused")
1279                .screen("game_over")
1280                .build();
1281
1282            // Simulate a test session that covers everything
1283            // Title screen
1284            tracker.record_state(StateId::new("screen", "title"));
1285            tracker.record_interaction(
1286                &ElementId::new("button", "start_game"),
1287                InteractionType::Click,
1288            );
1289
1290            // Playing
1291            tracker.record_state(StateId::new("screen", "playing"));
1292            tracker.record_interaction(&ElementId::new("paddle", "player"), InteractionType::Click);
1293            tracker.record_interaction(&ElementId::new("button", "pause"), InteractionType::Click);
1294
1295            // Paused
1296            tracker.record_state(StateId::new("screen", "paused"));
1297
1298            // Resume and game over
1299            tracker.record_state(StateId::new("screen", "game_over"));
1300            tracker
1301                .record_interaction(&ElementId::new("button", "restart"), InteractionType::Click);
1302
1303            // Verify 100% coverage
1304            assert!(tracker.assert_complete().is_ok());
1305        }
1306
1307        #[test]
1308        fn test_pong_partial_coverage() {
1309            let mut tracker = UxCoverageBuilder::new()
1310                .button("start_game")
1311                .button("pause")
1312                .screen("title")
1313                .screen("playing")
1314                .build();
1315
1316            // Only cover some things
1317            tracker.record_state(StateId::new("screen", "title"));
1318            tracker.record_interaction(
1319                &ElementId::new("button", "start_game"),
1320                InteractionType::Click,
1321            );
1322
1323            let report = tracker.generate_report();
1324            assert!(!report.is_complete);
1325            assert!((report.element_coverage - 0.5).abs() < f64::EPSILON);
1326            assert!((report.state_coverage - 0.5).abs() < f64::EPSILON);
1327        }
1328    }
1329
1330    // =========================================================================
1331    // Tests for SIMPLE CONVENIENCE API
1332    // =========================================================================
1333
1334    mod simple_api_tests {
1335        use super::*;
1336
1337        #[test]
1338        fn test_click_convenience() {
1339            let mut tracker = UxCoverageTracker::new();
1340            tracker.register_button("submit");
1341            tracker.click("submit");
1342            assert!(tracker.is_complete());
1343        }
1344
1345        #[test]
1346        fn test_input_convenience() {
1347            let mut tracker = UxCoverageTracker::new();
1348            tracker.register_input("username");
1349            tracker.input("username");
1350            assert!(tracker.is_complete());
1351        }
1352
1353        #[test]
1354        fn test_visit_convenience() {
1355            let mut tracker = UxCoverageTracker::new();
1356            tracker.register_screen("home");
1357            tracker.visit("home");
1358            assert!(tracker.is_complete());
1359        }
1360
1361        #[test]
1362        fn test_visit_modal_convenience() {
1363            let mut tracker = UxCoverageTracker::new();
1364            tracker.register_modal("confirm");
1365            tracker.visit_modal("confirm");
1366            assert!(tracker.is_complete());
1367        }
1368
1369        #[test]
1370        fn test_summary_elements_only() {
1371            let mut tracker = UxCoverageTracker::new();
1372            tracker.register_button("a");
1373            tracker.register_button("b");
1374            tracker.click("a");
1375            assert_eq!(tracker.summary(), "GUI: 50% (1/2 elements)");
1376        }
1377
1378        #[test]
1379        fn test_summary_screens_only() {
1380            let mut tracker = UxCoverageTracker::new();
1381            tracker.register_screen("home");
1382            tracker.register_screen("settings");
1383            tracker.visit("home");
1384            assert_eq!(tracker.summary(), "GUI: 50% (1/2 screens)");
1385        }
1386
1387        #[test]
1388        fn test_summary_both() {
1389            let mut tracker = UxCoverageTracker::new();
1390            tracker.register_button("btn");
1391            tracker.register_screen("home");
1392            tracker.click("btn");
1393            // 100% elements, 0% screens = 50% overall
1394            assert_eq!(tracker.summary(), "GUI: 50% (1/1 elements, 0/1 screens)");
1395        }
1396
1397        #[test]
1398        fn test_percent() {
1399            let mut tracker = UxCoverageTracker::new();
1400            tracker.register_button("a");
1401            tracker.register_button("b");
1402            tracker.click("a");
1403            assert!((tracker.percent() - 50.0).abs() < f64::EPSILON);
1404        }
1405
1406        #[test]
1407        fn test_meets_threshold() {
1408            let mut tracker = UxCoverageTracker::new();
1409            tracker.register_button("a");
1410            tracker.register_button("b");
1411            tracker.click("a");
1412            assert!(tracker.meets(50.0));
1413            assert!(!tracker.meets(51.0));
1414        }
1415
1416        #[test]
1417        fn test_calculator_coverage_preset() {
1418            let tracker = calculator_coverage();
1419            // Should have 20 buttons + 2 screens
1420            assert_eq!(tracker.elements.len(), 20);
1421            assert_eq!(tracker.expected_states.len(), 2);
1422        }
1423
1424        #[test]
1425        fn test_game_coverage_helper() {
1426            let tracker = game_coverage(
1427                &["start", "pause", "quit"],
1428                &["title", "playing", "game_over"],
1429            );
1430            assert_eq!(tracker.elements.len(), 3);
1431            assert_eq!(tracker.expected_states.len(), 3);
1432        }
1433    }
1434
1435    mod macro_tests {
1436        #[allow(unused_imports)]
1437        use super::*;
1438
1439        #[test]
1440        fn test_gui_coverage_macro_buttons_only() {
1441            let tracker = crate::gui_coverage! {
1442                buttons: ["a", "b", "c"]
1443            };
1444            assert_eq!(tracker.elements.len(), 3);
1445        }
1446
1447        #[test]
1448        fn test_gui_coverage_macro_full() {
1449            let mut tracker = crate::gui_coverage! {
1450                buttons: ["start", "stop"],
1451                inputs: ["name"],
1452                screens: ["home", "settings"],
1453                modals: ["confirm"]
1454            };
1455            assert_eq!(tracker.elements.len(), 3); // 2 buttons + 1 input
1456            assert_eq!(tracker.expected_states.len(), 3); // 2 screens + 1 modal
1457
1458            // Test the simple API works with macro-created tracker
1459            tracker.click("start");
1460            tracker.visit("home");
1461            assert!(tracker.percent() > 0.0);
1462        }
1463
1464        #[test]
1465        fn test_gui_coverage_macro_trailing_comma() {
1466            let tracker = crate::gui_coverage! {
1467                buttons: ["a", "b",],
1468                screens: ["home",],
1469            };
1470            assert_eq!(tracker.elements.len(), 2);
1471            assert_eq!(tracker.expected_states.len(), 1);
1472        }
1473    }
1474}