1use std::collections::{HashMap, HashSet};
42
43#[derive(Debug, Clone)]
45pub struct GuiCoverage {
46 name: String,
48 elements: HashSet<String>,
50 covered_elements: HashSet<String>,
52 screens: HashSet<String>,
54 covered_screens: HashSet<String>,
56 journeys: HashMap<String, Vec<String>>,
58 completed_journeys: HashSet<String>,
60 interaction_log: Vec<Interaction>,
62}
63
64#[derive(Debug, Clone)]
66pub struct Interaction {
67 pub kind: InteractionKind,
69 pub target: String,
71 pub value: Option<String>,
73 pub frame: u64,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum InteractionKind {
80 KeyPress,
82 Click,
84 Input,
86 Navigate,
88 View,
90}
91
92impl GuiCoverage {
93 #[must_use]
95 pub fn new(name: &str) -> Self {
96 Self {
97 name: name.to_string(),
98 elements: HashSet::new(),
99 covered_elements: HashSet::new(),
100 screens: HashSet::new(),
101 covered_screens: HashSet::new(),
102 journeys: HashMap::new(),
103 completed_journeys: HashSet::new(),
104 interaction_log: Vec::new(),
105 }
106 }
107
108 #[must_use]
110 pub fn tui_preset(name: &str) -> Self {
111 let mut coverage = Self::new(name);
112
113 coverage.register_element("title_bar");
115 coverage.register_element("status_bar");
116 coverage.register_element("help_text");
117 coverage.register_element("statistics_panel");
118 coverage.register_element("controls_panel");
119
120 coverage.register_screen("main_view");
122 coverage.register_screen("help_screen");
123
124 coverage
125 }
126
127 #[must_use]
129 pub fn tsp_tui() -> Self {
130 let mut coverage = Self::new("TSP GRASP TUI");
131
132 coverage.register_element("title_bar");
134 coverage.register_element("equations_panel");
135 coverage.register_element("city_plot");
136 coverage.register_element("convergence_graph");
137 coverage.register_element("statistics_panel");
138 coverage.register_element("controls_panel");
139 coverage.register_element("status_bar");
140
141 coverage.register_element("tour_length_display");
143 coverage.register_element("best_tour_display");
144 coverage.register_element("lower_bound_display");
145 coverage.register_element("gap_display");
146 coverage.register_element("crossings_display");
147 coverage.register_element("restarts_display");
148 coverage.register_element("method_display");
149 coverage.register_element("rcl_display");
150
151 coverage.register_element("space_toggle");
153 coverage.register_element("g_step");
154 coverage.register_element("r_reset");
155 coverage.register_element("plus_rcl");
156 coverage.register_element("minus_rcl");
157 coverage.register_element("m_method");
158 coverage.register_element("q_quit");
159
160 coverage.register_screen("main_view");
162 coverage.register_screen("running_state");
163 coverage.register_screen("paused_state");
164 coverage.register_screen("converged_state");
165
166 coverage.register_journey(
168 "basic_run",
169 vec!["main_view", "space_toggle", "running_state"],
170 );
171 coverage.register_journey(
172 "single_step",
173 vec!["main_view", "g_step", "tour_length_display"],
174 );
175 coverage.register_journey(
176 "change_method",
177 vec!["main_view", "m_method", "method_display"],
178 );
179 coverage.register_journey("adjust_rcl", vec!["main_view", "plus_rcl", "rcl_display"]);
180 coverage.register_journey(
181 "full_convergence",
182 vec![
183 "main_view",
184 "space_toggle",
185 "running_state",
186 "converged_state",
187 ],
188 );
189
190 coverage
191 }
192
193 #[must_use]
197 pub fn tsp_wasm() -> Self {
198 let mut coverage = Self::new("TSP GRASP WASM");
199
200 coverage.register_element("header"); coverage.register_element("equations_panel"); coverage.register_element("tsp_canvas"); coverage.register_element("sparkline"); coverage.register_element("statistics_panel"); coverage.register_element("controls_panel"); coverage.register_element("footer"); coverage.register_element("stat_best"); coverage.register_element("eq_tour"); coverage.register_element("stat_lb"); coverage.register_element("stat_gap"); coverage.register_element("fals_status"); coverage.register_element("stat_restarts"); coverage.register_element("select_method"); coverage.register_element("slider_n"); coverage.register_element("btn_play"); coverage.register_element("btn_step"); coverage.register_element("btn_reset"); coverage.register_element("btn_run10"); coverage.register_element("btn_run100"); coverage.register_element("select_method"); coverage.register_element("btn_run_tests"); coverage.register_screen("main_view"); coverage.register_screen("running_state"); coverage.register_screen("paused_state"); coverage.register_screen("converged_state"); coverage.register_journey("basic_run", vec!["main_view", "btn_play", "running_state"]);
236 coverage.register_journey("single_step", vec!["main_view", "btn_step", "stat_best"]);
237 coverage.register_journey(
238 "change_method",
239 vec!["main_view", "select_method", "stat_best"],
240 );
241 coverage.register_journey("adjust_cities", vec!["main_view", "slider_n", "tsp_canvas"]);
242 coverage.register_journey(
243 "full_convergence",
244 vec!["main_view", "btn_play", "running_state", "converged_state"],
245 );
246
247 coverage
248 }
249
250 pub fn register_element(&mut self, name: &str) {
256 self.elements.insert(name.to_string());
257 }
258
259 pub fn register_screen(&mut self, name: &str) {
261 self.screens.insert(name.to_string());
262 }
263
264 pub fn register_journey(&mut self, name: &str, steps: Vec<&str>) {
266 contract_pre_iterator!(name);
267 self.journeys.insert(
268 name.to_string(),
269 steps.into_iter().map(String::from).collect(),
270 );
271 }
272
273 pub fn cover_element(&mut self, name: &str) {
279 if self.elements.contains(name) {
280 self.covered_elements.insert(name.to_string());
281 }
282 }
283
284 pub fn cover_screen(&mut self, name: &str) {
286 if self.screens.contains(name) {
287 self.covered_screens.insert(name.to_string());
288 }
289 }
290
291 pub fn complete_journey(&mut self, name: &str) {
293 if self.journeys.contains_key(name) {
294 self.completed_journeys.insert(name.to_string());
295 if let Some(steps) = self.journeys.get(name).cloned() {
297 for step in steps {
298 self.cover_element(&step);
299 self.cover_screen(&step);
300 }
301 }
302 }
303 }
304
305 pub fn log_interaction(
307 &mut self,
308 kind: InteractionKind,
309 target: &str,
310 value: Option<&str>,
311 frame: u64,
312 ) {
313 self.interaction_log.push(Interaction {
314 kind,
315 target: target.to_string(),
316 value: value.map(String::from),
317 frame,
318 });
319
320 match kind {
322 InteractionKind::Navigate => {
323 self.cover_screen(target);
324 }
325 InteractionKind::KeyPress
326 | InteractionKind::Click
327 | InteractionKind::Input
328 | InteractionKind::View => {
329 self.cover_element(target);
330 }
331 }
332 }
333
334 #[must_use]
340 pub fn element_coverage(&self) -> f64 {
341 if self.elements.is_empty() {
342 return 1.0;
343 }
344 self.covered_elements.len() as f64 / self.elements.len() as f64
345 }
346
347 #[must_use]
349 pub fn screen_coverage(&self) -> f64 {
350 if self.screens.is_empty() {
351 return 1.0;
352 }
353 self.covered_screens.len() as f64 / self.screens.len() as f64
354 }
355
356 #[must_use]
358 pub fn journey_coverage(&self) -> f64 {
359 if self.journeys.is_empty() {
360 return 1.0;
361 }
362 self.completed_journeys.len() as f64 / self.journeys.len() as f64
363 }
364
365 #[must_use]
367 pub fn total_coverage(&self) -> f64 {
368 let element = self.element_coverage();
369 let screen = self.screen_coverage();
370 let journey = self.journey_coverage();
371
372 element * 0.5 + screen * 0.3 + journey * 0.2
374 }
375
376 #[must_use]
378 pub fn meets_threshold(&self, threshold: f64) -> bool {
379 self.total_coverage() >= threshold
380 }
381
382 #[must_use]
384 pub fn is_complete(&self) -> bool {
385 self.element_coverage() >= 1.0
386 && self.screen_coverage() >= 1.0
387 && self.journey_coverage() >= 1.0
388 }
389
390 #[must_use]
396 pub fn summary(&self) -> String {
397 format!(
398 "GUI: {:.0}% ({}/{} elements, {}/{} screens)",
399 self.total_coverage() * 100.0,
400 self.covered_elements.len(),
401 self.elements.len(),
402 self.covered_screens.len(),
403 self.screens.len()
404 )
405 }
406
407 #[must_use]
409 pub fn uncovered_elements(&self) -> Vec<&str> {
410 self.elements
411 .iter()
412 .filter(|e| !self.covered_elements.contains(*e))
413 .map(String::as_str)
414 .collect()
415 }
416
417 #[must_use]
419 pub fn uncovered_screens(&self) -> Vec<&str> {
420 self.screens
421 .iter()
422 .filter(|s| !self.covered_screens.contains(*s))
423 .map(String::as_str)
424 .collect()
425 }
426
427 #[must_use]
429 pub fn incomplete_journeys(&self) -> Vec<&str> {
430 self.journeys
431 .keys()
432 .filter(|j| !self.completed_journeys.contains(*j))
433 .map(String::as_str)
434 .collect()
435 }
436
437 #[must_use]
439 pub fn element_count(&self) -> usize {
440 self.elements.len()
441 }
442
443 #[must_use]
445 pub fn screen_count(&self) -> usize {
446 self.screens.len()
447 }
448
449 #[must_use]
451 pub fn journey_count(&self) -> usize {
452 self.journeys.len()
453 }
454
455 #[must_use]
457 pub fn has_element(&self, name: &str) -> bool {
458 self.elements.contains(name)
459 }
460
461 #[must_use]
463 pub fn has_screen(&self, name: &str) -> bool {
464 self.screens.contains(name)
465 }
466
467 #[must_use]
469 pub fn detailed_report(&self) -> String {
470 use std::fmt::Write;
471 let mut report = String::new();
472
473 let _ = writeln!(report, "=== GUI Coverage Report: {} ===\n", self.name);
474 let _ = writeln!(report, "Overall: {:.1}%", self.total_coverage() * 100.0);
475 let _ = writeln!(
476 report,
477 " Elements: {:.1}% ({}/{})",
478 self.element_coverage() * 100.0,
479 self.covered_elements.len(),
480 self.elements.len()
481 );
482 let _ = writeln!(
483 report,
484 " Screens: {:.1}% ({}/{})",
485 self.screen_coverage() * 100.0,
486 self.covered_screens.len(),
487 self.screens.len()
488 );
489 let _ = writeln!(
490 report,
491 " Journeys: {:.1}% ({}/{})",
492 self.journey_coverage() * 100.0,
493 self.completed_journeys.len(),
494 self.journeys.len()
495 );
496
497 let uncovered_elements = self.uncovered_elements();
498 if !uncovered_elements.is_empty() {
499 report.push_str("\nUncovered Elements:\n");
500 for elem in uncovered_elements {
501 let _ = writeln!(report, " - {elem}");
502 }
503 }
504
505 let uncovered_screens = self.uncovered_screens();
506 if !uncovered_screens.is_empty() {
507 report.push_str("\nUncovered Screens:\n");
508 for screen in uncovered_screens {
509 let _ = writeln!(report, " - {screen}");
510 }
511 }
512
513 let incomplete = self.incomplete_journeys();
514 if !incomplete.is_empty() {
515 report.push_str("\nIncomplete Journeys:\n");
516 for journey in incomplete {
517 let _ = writeln!(report, " - {journey}");
518 }
519 }
520
521 report
522 }
523
524 #[must_use]
526 pub fn interaction_count(&self) -> usize {
527 self.interaction_log.len()
528 }
529
530 #[must_use]
532 pub fn name(&self) -> &str {
533 &self.name
534 }
535}
536
537#[macro_export]
539macro_rules! gui_coverage {
540 ($name:expr => elements: [$($elem:expr),* $(,)?], screens: [$($screen:expr),* $(,)?]) => {{
541 let mut coverage = $crate::edd::gui_coverage::GuiCoverage::new($name);
542 $(coverage.register_element($elem);)*
543 $(coverage.register_screen($screen);)*
544 coverage
545 }};
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551
552 #[test]
553 fn test_new_coverage() {
554 let coverage = GuiCoverage::new("Test");
555 assert_eq!(coverage.name(), "Test");
556 assert!(coverage.elements.is_empty());
557 assert!(coverage.screens.is_empty());
558 }
559
560 #[test]
561 fn test_register_and_cover_elements() {
562 let mut coverage = GuiCoverage::new("Test");
563 coverage.register_element("button1");
564 coverage.register_element("button2");
565
566 assert_eq!(coverage.element_coverage(), 0.0);
567
568 coverage.cover_element("button1");
569 assert!((coverage.element_coverage() - 0.5).abs() < f64::EPSILON);
570
571 coverage.cover_element("button2");
572 assert!((coverage.element_coverage() - 1.0).abs() < f64::EPSILON);
573 }
574
575 #[test]
576 fn test_register_and_cover_screens() {
577 let mut coverage = GuiCoverage::new("Test");
578 coverage.register_screen("main");
579 coverage.register_screen("settings");
580
581 assert_eq!(coverage.screen_coverage(), 0.0);
582
583 coverage.cover_screen("main");
584 assert!((coverage.screen_coverage() - 0.5).abs() < f64::EPSILON);
585 }
586
587 #[test]
588 fn test_journey_completion() {
589 let mut coverage = GuiCoverage::new("Test");
590 coverage.register_element("btn");
591 coverage.register_screen("main");
592 coverage.register_journey("flow", vec!["main", "btn"]);
593
594 coverage.complete_journey("flow");
595
596 assert!(coverage.completed_journeys.contains("flow"));
597 assert!(coverage.covered_elements.contains("btn"));
598 assert!(coverage.covered_screens.contains("main"));
599 }
600
601 #[test]
602 fn test_tsp_tui_preset() {
603 let coverage = GuiCoverage::tsp_tui();
604
605 assert!(coverage.elements.contains("city_plot"));
607 assert!(coverage.elements.contains("convergence_graph"));
608 assert!(coverage.elements.contains("equations_panel"));
609
610 assert!(coverage.screens.contains("main_view"));
612 assert!(coverage.screens.contains("converged_state"));
613
614 assert!(coverage.journeys.contains_key("basic_run"));
616 assert!(coverage.journeys.contains_key("full_convergence"));
617 }
618
619 #[test]
620 fn test_tsp_wasm_preset() {
621 let coverage = GuiCoverage::tsp_wasm();
622
623 assert!(coverage.elements.contains("tsp_canvas"));
625 assert!(coverage.elements.contains("sparkline"));
626 assert!(coverage.elements.contains("btn_play"));
627 assert!(coverage.elements.contains("btn_step"));
628 assert!(coverage.elements.contains("stat_best"));
629
630 assert!(coverage.screens.contains("main_view"));
632 assert!(coverage.screens.contains("running_state"));
633 assert!(coverage.screens.contains("paused_state"));
634 assert!(coverage.screens.contains("converged_state"));
635
636 assert!(coverage.journeys.contains_key("basic_run"));
638 assert!(coverage.journeys.contains_key("full_convergence"));
639 assert!(coverage.journeys.contains_key("single_step"));
640 }
641
642 #[test]
643 fn test_summary() {
644 let mut coverage = GuiCoverage::new("Test");
645 coverage.register_element("e1");
646 coverage.register_element("e2");
647 coverage.register_screen("s1");
648
649 coverage.cover_element("e1");
650
651 let summary = coverage.summary();
652 assert!(summary.contains("1/2 elements"));
653 assert!(summary.contains("0/1 screens"));
654 }
655
656 #[test]
657 fn test_detailed_report() {
658 let mut coverage = GuiCoverage::new("Test");
659 coverage.register_element("covered");
660 coverage.register_element("uncovered");
661 coverage.cover_element("covered");
662
663 let report = coverage.detailed_report();
664 assert!(report.contains("Test"));
665 assert!(report.contains("uncovered"));
666 }
667
668 #[test]
669 fn test_meets_threshold() {
670 let mut coverage = GuiCoverage::new("Test");
671 coverage.register_element("e1");
672 coverage.register_element("e2");
673 coverage.cover_element("e1");
674 coverage.cover_element("e2");
675
676 assert!(coverage.meets_threshold(0.5));
677 }
678
679 #[test]
680 fn test_interaction_logging() {
681 let mut coverage = GuiCoverage::new("Test");
682 coverage.register_element("btn");
683
684 coverage.log_interaction(InteractionKind::Click, "btn", None, 1);
685
686 assert_eq!(coverage.interaction_count(), 1);
687 assert!(coverage.covered_elements.contains("btn"));
688 }
689
690 #[test]
691 fn test_gui_coverage_macro() {
692 let coverage = gui_coverage!("Test" =>
693 elements: ["btn1", "btn2"],
694 screens: ["main"]
695 );
696
697 assert!(coverage.elements.contains("btn1"));
698 assert!(coverage.elements.contains("btn2"));
699 assert!(coverage.screens.contains("main"));
700 }
701}