1use crate::result::{ProbarError, ProbarResult};
36use serde::{Deserialize, Serialize};
37use std::collections::{HashMap, HashSet};
38use std::fmt;
39
40#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
42pub struct ElementId {
43 pub element_type: String,
45 pub id: String,
47 pub parent: Option<String>,
49}
50
51impl ElementId {
52 #[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 #[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 #[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#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
90pub enum InteractionType {
91 Click,
93 Focus,
95 Blur,
97 Input,
99 Hover,
101 Scroll,
103 DragStart,
105 DragEnd,
107 KeyPress(String),
109 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#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct TrackedInteraction {
133 pub element: ElementId,
135 pub interaction: InteractionType,
137 pub count: u64,
139}
140
141#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
143pub struct StateId {
144 pub category: String,
146 pub name: String,
148}
149
150impl StateId {
151 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct ElementCoverage {
170 pub element: ElementId,
172 pub tested_interactions: HashSet<InteractionType>,
174 pub expected_interactions: HashSet<InteractionType>,
176 pub was_visible: bool,
178 pub was_reachable: bool,
180}
181
182impl ElementCoverage {
183 #[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 pub fn expect(&mut self, interaction: InteractionType) {
197 self.expected_interactions.insert(interaction);
198 }
199
200 pub fn record(&mut self, interaction: InteractionType) {
202 self.tested_interactions.insert(interaction);
203 }
204
205 pub fn mark_visible(&mut self) {
207 self.was_visible = true;
208 }
209
210 pub fn mark_reachable(&mut self) {
212 self.was_reachable = true;
213 }
214
215 #[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 #[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 #[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#[derive(Debug, Default)]
248pub struct UxCoverageTracker {
249 elements: HashMap<String, ElementCoverage>,
251 visited_states: HashSet<StateId>,
253 expected_states: HashSet<StateId>,
255 interaction_counts: HashMap<String, u64>,
257 journeys: Vec<Vec<StateId>>,
259 current_journey: Vec<StateId>,
261}
262
263impl UxCoverageTracker {
264 #[must_use]
266 pub fn new() -> Self {
267 Self::default()
268 }
269
270 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 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 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 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 pub fn register_state(&mut self, state: StateId) {
307 self.expected_states.insert(state);
308 }
309
310 pub fn register_screen(&mut self, name: &str) {
312 self.register_state(StateId::new("screen", name));
313 }
314
315 pub fn register_modal(&mut self, name: &str) {
317 self.register_state(StateId::new("modal", name));
318 }
319
320 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 let count_key = format!("{}:{}", key, interaction);
330 *self.interaction_counts.entry(count_key).or_insert(0) += 1;
331 }
332
333 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 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 pub fn record_state(&mut self, state: StateId) {
351 self.visited_states.insert(state.clone());
352 self.current_journey.push(state);
353 }
354
355 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 #[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 #[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 #[must_use]
393 pub fn overall_coverage(&self) -> f64 {
394 let element = self.element_coverage();
395 let state = self.state_coverage();
396
397 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 #[must_use]
410 pub fn is_complete(&self) -> bool {
411 (self.overall_coverage() - 1.0).abs() < f64::EPSILON
412 }
413
414 #[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 #[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 #[must_use]
434 pub fn journeys(&self) -> &[Vec<StateId>] {
435 &self.journeys
436 }
437
438 #[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 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 pub fn assert_complete(&self) -> ProbarResult<()> {
492 self.assert_coverage(1.0)
493 }
494
495 pub fn click(&mut self, id: &str) {
510 let element = ElementId::new("button", id);
511 self.record_interaction(&element, InteractionType::Click);
512 }
513
514 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 pub fn visit(&mut self, screen: &str) {
524 self.record_state(StateId::new("screen", screen));
525 }
526
527 pub fn visit_modal(&mut self, modal: &str) {
529 self.record_state(StateId::new("modal", modal));
530 }
531
532 #[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 #[must_use]
566 pub fn percent(&self) -> f64 {
567 self.overall_coverage() * 100.0
568 }
569
570 #[must_use]
572 pub fn meets(&self, threshold_percent: f64) -> bool {
573 self.percent() >= threshold_percent
574 }
575}
576
577#[derive(Debug, Clone, Serialize, Deserialize)]
579pub struct UxCoverageReport {
580 pub overall_coverage: f64,
582 pub element_coverage: f64,
584 pub state_coverage: f64,
586 pub total_elements: usize,
588 pub covered_elements: usize,
590 pub total_states: usize,
592 pub covered_states: usize,
594 pub total_interactions: u64,
596 pub unique_journeys: usize,
598 pub is_complete: bool,
600}
601
602impl UxCoverageReport {
603 #[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#[derive(Debug, Default)]
641pub struct UxCoverageBuilder {
642 tracker: UxCoverageTracker,
643}
644
645impl UxCoverageBuilder {
646 #[must_use]
648 pub fn new() -> Self {
649 Self::default()
650 }
651
652 #[must_use]
654 pub fn button(mut self, id: &str) -> Self {
655 self.tracker.register_button(id);
656 self
657 }
658
659 #[must_use]
661 pub fn input(mut self, id: &str) -> Self {
662 self.tracker.register_input(id);
663 self
664 }
665
666 #[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 #[must_use]
675 pub fn screen(mut self, name: &str) -> Self {
676 self.tracker.register_screen(name);
677 self
678 }
679
680 #[must_use]
682 pub fn modal(mut self, name: &str) -> Self {
683 self.tracker.register_modal(name);
684 self
685 }
686
687 #[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 #[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 #[must_use]
703 pub fn build(self) -> UxCoverageTracker {
704 self.tracker
705 }
706}
707
708#[macro_export]
736macro_rules! gui_coverage {
737 {
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#[must_use]
769pub fn calculator_coverage() -> UxCoverageTracker {
770 UxCoverageBuilder::new()
771 .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 .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 .screen("calculator")
795 .screen("history")
796 .build()
797}
798
799#[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()); }
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 tracker.register_button("btn1");
981 tracker.register_button("btn2");
982 tracker.register_screen("home");
983 tracker.register_screen("settings");
984
985 tracker.record_interaction(&ElementId::new("button", "btn1"), InteractionType::Click);
987 tracker.record_state(StateId::new("screen", "home"));
988
989 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 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 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 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 tracker.record_state(StateId::new("screen", "title"));
1285 tracker.record_interaction(
1286 &ElementId::new("button", "start_game"),
1287 InteractionType::Click,
1288 );
1289
1290 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 tracker.record_state(StateId::new("screen", "paused"));
1297
1298 tracker.record_state(StateId::new("screen", "game_over"));
1300 tracker
1301 .record_interaction(&ElementId::new("button", "restart"), InteractionType::Click);
1302
1303 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 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 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 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 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); assert_eq!(tracker.expected_states.len(), 3); 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}