1use crate::stack::types::{CrateInfo, CrateStatus, StackHealthReport};
11use anyhow::Result;
12#[cfg(feature = "presentar-terminal")]
13use std::io::{self, Write};
14#[cfg(feature = "presentar-terminal")]
15use std::time::Duration;
16
17#[cfg(feature = "presentar-terminal")]
18use crossterm::{
19 cursor,
20 event::{self, Event, KeyCode, KeyEventKind},
21 execute,
22 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
23};
24
25#[cfg(feature = "presentar-terminal")]
26use presentar_terminal::{CellBuffer, Color, DiffRenderer, Modifiers};
27
28#[cfg(feature = "presentar-terminal")]
30const CYAN: Color = Color { r: 0.0, g: 1.0, b: 1.0, a: 1.0 };
31
32#[cfg(feature = "presentar-terminal")]
34pub struct Dashboard {
35 report: StackHealthReport,
37 selected: usize,
39 show_details: bool,
41 buffer: CellBuffer,
43 renderer: DiffRenderer,
45 width: u16,
47 height: u16,
49}
50
51#[cfg(feature = "presentar-terminal")]
52impl Dashboard {
53 pub fn new(report: StackHealthReport) -> Self {
55 let (width, height) = crossterm::terminal::size().unwrap_or((80, 24));
56 Self {
57 report,
58 selected: 0,
59 show_details: true,
60 buffer: CellBuffer::new(width, height),
61 renderer: DiffRenderer::new(),
62 width,
63 height,
64 }
65 }
66
67 pub fn run(&mut self) -> Result<()> {
69 enable_raw_mode()?;
71 let mut stdout = io::stdout();
72 execute!(stdout, EnterAlternateScreen, cursor::Hide)?;
73
74 let result = self.run_loop(&mut stdout);
76
77 disable_raw_mode()?;
79 execute!(stdout, LeaveAlternateScreen, cursor::Show)?;
80
81 result
82 }
83
84 fn run_loop(&mut self, stdout: &mut io::Stdout) -> Result<()> {
86 loop {
87 let (w, h) = crossterm::terminal::size().unwrap_or((80, 24));
89 if w != self.width || h != self.height {
90 self.width = w;
91 self.height = h;
92 self.buffer.resize(w, h);
93 self.renderer.reset();
94 }
95
96 self.buffer.clear();
98 self.render();
99
100 self.renderer.flush(&mut self.buffer, stdout)?;
102 stdout.flush()?;
103
104 if !event::poll(Duration::from_millis(100))? {
106 continue;
107 }
108 let Event::Key(key) = event::read()? else { continue };
109 if key.kind != KeyEventKind::Press {
110 continue;
111 }
112 match key.code {
113 KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
114 KeyCode::Up | KeyCode::Char('k') => {
115 if self.selected > 0 {
116 self.selected -= 1;
117 }
118 }
119 KeyCode::Down | KeyCode::Char('j') => {
120 if self.selected < self.report.crates.len().saturating_sub(1) {
121 self.selected += 1;
122 }
123 }
124 KeyCode::Char('d') => {
125 self.show_details = !self.show_details;
126 }
127 _ => {}
128 }
129 }
130 }
131
132 fn write_str(&mut self, x: u16, y: u16, s: &str, fg: Color) {
134 let mut cx = x;
135 for ch in s.chars() {
136 if cx >= self.width {
137 break;
138 }
139 let mut buf = [0u8; 4];
140 let s = ch.encode_utf8(&mut buf);
141 self.buffer.update(cx, y, s, fg, Color::TRANSPARENT, Modifiers::NONE);
142 cx = cx.saturating_add(1);
143 }
144 }
145
146 fn set_char(&mut self, x: u16, y: u16, ch: char, fg: Color) {
148 if x < self.width && y < self.height {
149 let mut buf = [0u8; 4];
150 let s = ch.encode_utf8(&mut buf);
151 self.buffer.update(x, y, s, fg, Color::TRANSPARENT, Modifiers::NONE);
152 }
153 }
154
155 fn render(&mut self) {
157 let w = self.width;
158 let h = self.height;
159
160 let title_h: u16 = 3;
162 let help_h: u16 = 3;
163 let details_h: u16 = if self.show_details { 8 } else { 0 };
164 let table_h = h.saturating_sub(title_h + details_h + help_h);
165
166 self.render_title(0, 0, w, title_h);
167 self.render_table(0, title_h, w, table_h);
168
169 if self.show_details {
170 self.render_details(0, title_h + table_h, w, details_h);
171 self.render_help(0, h.saturating_sub(help_h), w, help_h);
172 } else {
173 self.render_help(0, h.saturating_sub(help_h), w, help_h);
174 }
175 }
176
177 fn render_title(&mut self, x: u16, y: u16, w: u16, h: u16) {
179 self.draw_box(x, y, w, h, "");
180
181 let summary = &self.report.summary;
182 let status_text = if summary.error_count > 0 {
183 format!("X {} errors", summary.error_count)
184 } else if summary.warning_count > 0 {
185 format!("! {} warnings", summary.warning_count)
186 } else {
187 "* All healthy".to_string()
188 };
189
190 let title =
191 format!("PAIML Stack Dashboard | {} crates | {}", summary.total_crates, status_text);
192
193 let max_len = (w.saturating_sub(4)) as usize;
194 self.write_str(x + 2, y + 1, &title[..title.len().min(max_len)], CYAN);
195 }
196
197 fn render_table(&mut self, x: u16, y: u16, w: u16, h: u16) {
199 self.draw_box(x, y, w, h, " Crates ");
200
201 let header =
203 format!("{:4} {:15} {:12} {:12} {:8}", "Stat", "Crate", "Local", "Crates.io", "Issues");
204 let max_len = (w.saturating_sub(2)) as usize;
205 self.write_str(x + 1, y + 1, &header[..header.len().min(max_len)], Color::YELLOW);
206
207 let sep = "─".repeat(max_len);
209 self.write_str(x + 1, y + 2, &sep[..sep.len().min(max_len)], Color::WHITE);
210
211 let content_y = y + 3;
213 let content_h = h.saturating_sub(4) as usize;
214
215 let rows: Vec<_> = self
216 .report
217 .crates
218 .iter()
219 .take(content_h)
220 .enumerate()
221 .map(|(i, crate_info)| {
222 let status_icon = match crate_info.status {
223 CrateStatus::Healthy => "*",
224 CrateStatus::Warning => "!",
225 CrateStatus::Error => "X",
226 CrateStatus::Unknown => "?",
227 };
228
229 let crates_io = crate_info
230 .crates_io_version
231 .as_ref()
232 .map(|v| v.to_string())
233 .unwrap_or_else(|| "—".to_string());
234
235 let issue_count = crate_info.issues.len();
236 let issue_str =
237 if issue_count > 0 { issue_count.to_string() } else { "—".to_string() };
238
239 let is_selected = i == self.selected;
240 let fg_color = if is_selected { Color::YELLOW } else { Color::WHITE };
241
242 let row = format!(
243 "{:4} {:15} {:12} {:12} {:8}",
244 status_icon,
245 &crate_info.name[..crate_info.name.len().min(15)],
246 crate_info.local_version.to_string(),
247 crates_io,
248 issue_str
249 );
250
251 let marker = if is_selected { "> " } else { " " };
252 let full_row = format!("{}{}", marker, row);
253 (i, full_row, fg_color)
254 })
255 .collect();
256
257 for (i, full_row, fg_color) in rows {
258 self.write_str(
259 x + 1,
260 content_y + i as u16,
261 &full_row[..full_row.len().min(max_len)],
262 fg_color,
263 );
264 }
265 }
266
267 fn render_details(&mut self, x: u16, y: u16, w: u16, h: u16) {
269 self.draw_box(x, y, w, h, " Details ");
270 let max_len = (w.saturating_sub(2)) as usize;
271 let content_y = y + 1;
272
273 let crate_data = self.report.crates.get(self.selected).map(|c| {
275 let name_line = format!("Name: {}", c.name);
276 let version_line = format!("Version: {}", c.local_version);
277 let issue_lines: Vec<String> = c
278 .issues
279 .iter()
280 .take(h.saturating_sub(5) as usize)
281 .map(|issue| format!(" * {}", issue.message))
282 .collect();
283 (name_line, version_line, issue_lines)
284 });
285
286 if let Some((name_line, version_line, issue_lines)) = crate_data {
287 self.write_str(
289 x + 1,
290 content_y,
291 &name_line[..name_line.len().min(max_len)],
292 Color::WHITE,
293 );
294
295 self.write_str(
297 x + 1,
298 content_y + 1,
299 &version_line[..version_line.len().min(max_len)],
300 Color::WHITE,
301 );
302
303 if !issue_lines.is_empty() {
305 self.write_str(x + 1, content_y + 2, "Issues:", Color::RED);
306 for (i, issue_line) in issue_lines.iter().enumerate() {
307 self.write_str(
308 x + 1,
309 content_y + 3 + i as u16,
310 &issue_line[..issue_line.len().min(max_len)],
311 Color::WHITE,
312 );
313 }
314 } else {
315 self.write_str(x + 1, content_y + 2, "No issues", Color::GREEN);
316 }
317 } else {
318 self.write_str(x + 1, y + 1, "No crate selected", Color::WHITE);
319 }
320 }
321
322 fn render_help(&mut self, x: u16, y: u16, w: u16, h: u16) {
324 self.draw_box(x, y, w, h, "");
325
326 let help = "^/k Up v/j Down d Toggle details q/Esc Quit";
327 let max_len = (w.saturating_sub(2)) as usize;
328 self.write_str(x + 1, y + 1, &help[..help.len().min(max_len)], CYAN);
329 }
330
331 fn draw_box(&mut self, x: u16, y: u16, w: u16, h: u16, title: &str) {
333 if w < 2 || h < 2 {
334 return;
335 }
336
337 self.set_char(x, y, '┌', Color::WHITE);
339 for i in 1..w - 1 {
340 self.set_char(x + i, y, '─', Color::WHITE);
341 }
342 self.set_char(x + w - 1, y, '┐', Color::WHITE);
343
344 if !title.is_empty() && w > title.len() as u16 + 2 {
346 let title_x = x + 2;
347 self.write_str(title_x, y, title, CYAN);
348 }
349
350 for i in 1..h - 1 {
352 self.set_char(x, y + i, '│', Color::WHITE);
353 self.set_char(x + w - 1, y + i, '│', Color::WHITE);
354 }
355
356 self.set_char(x, y + h - 1, '└', Color::WHITE);
358 for i in 1..w - 1 {
359 self.set_char(x + i, y + h - 1, '─', Color::WHITE);
360 }
361 self.set_char(x + w - 1, y + h - 1, '┘', Color::WHITE);
362 }
363}
364
365#[cfg(feature = "presentar-terminal")]
367pub fn run_dashboard(report: StackHealthReport) -> Result<()> {
368 let mut dashboard = Dashboard::new(report);
369 dashboard.run()
370}
371
372#[cfg(all(test, feature = "presentar-terminal"))]
373mod tests {
374 use super::*;
375 use crate::stack::types::{CrateIssue, HealthSummary, IssueSeverity, IssueType};
376 use std::path::PathBuf;
377
378 fn create_test_report() -> StackHealthReport {
379 let mut crates = vec![
380 CrateInfo::new("trueno", semver::Version::new(1, 2, 0), PathBuf::new()),
381 CrateInfo::new("aprender", semver::Version::new(0, 14, 1), PathBuf::new()),
382 ];
383
384 crates[0].status = CrateStatus::Healthy;
385 crates[0].crates_io_version = Some(semver::Version::new(1, 2, 0));
386
387 crates[1].status = CrateStatus::Warning;
388 crates[1].crates_io_version = Some(semver::Version::new(0, 14, 1));
389 crates[1].issues.push(CrateIssue::new(
390 IssueSeverity::Warning,
391 IssueType::VersionBehind,
392 "Test warning",
393 ));
394
395 StackHealthReport {
396 crates,
397 conflicts: vec![],
398 summary: HealthSummary {
399 total_crates: 2,
400 healthy_count: 1,
401 warning_count: 1,
402 error_count: 0,
403 path_dependency_count: 0,
404 conflict_count: 0,
405 },
406 timestamp: chrono::Utc::now(),
407 }
408 }
409
410 #[test]
411 fn test_dashboard_creation() {
412 let report = create_test_report();
413 let dashboard = Dashboard::new(report);
414
415 assert_eq!(dashboard.selected, 0);
416 assert!(dashboard.show_details);
417 assert_eq!(dashboard.report.crates.len(), 2);
418 }
419
420 #[test]
421 fn test_dashboard_navigation() {
422 let report = create_test_report();
423 let mut dashboard = Dashboard::new(report);
424
425 assert_eq!(dashboard.selected, 0);
426 dashboard.selected = 1;
427 assert_eq!(dashboard.selected, 1);
428 dashboard.selected = 0;
429 assert_eq!(dashboard.selected, 0);
430 }
431
432 #[test]
433 fn test_dashboard_toggle_details() {
434 let report = create_test_report();
435 let mut dashboard = Dashboard::new(report);
436
437 assert!(dashboard.show_details);
438 dashboard.show_details = !dashboard.show_details;
439 assert!(!dashboard.show_details);
440 }
441
442 #[test]
443 fn test_dashboard_with_empty_report() {
444 let report = StackHealthReport {
445 crates: vec![],
446 conflicts: vec![],
447 summary: HealthSummary {
448 total_crates: 0,
449 healthy_count: 0,
450 warning_count: 0,
451 error_count: 0,
452 path_dependency_count: 0,
453 conflict_count: 0,
454 },
455 timestamp: chrono::Utc::now(),
456 };
457
458 let dashboard = Dashboard::new(report);
459 assert_eq!(dashboard.report.crates.len(), 0);
460 }
461
462 #[test]
463 fn test_dashboard_with_errors() {
464 let mut crates =
465 vec![CrateInfo::new("broken", semver::Version::new(0, 1, 0), PathBuf::new())];
466 crates[0].status = CrateStatus::Error;
467 crates[0].issues.push(CrateIssue::new(
468 IssueSeverity::Error,
469 IssueType::PathDependency,
470 "Path dependency error",
471 ));
472
473 let report = StackHealthReport {
474 crates,
475 conflicts: vec![],
476 summary: HealthSummary {
477 total_crates: 1,
478 healthy_count: 0,
479 warning_count: 0,
480 error_count: 1,
481 path_dependency_count: 1,
482 conflict_count: 0,
483 },
484 timestamp: chrono::Utc::now(),
485 };
486
487 let dashboard = Dashboard::new(report);
488 assert_eq!(dashboard.report.summary.error_count, 1);
489 }
490
491 #[test]
492 fn test_dashboard_selected_boundary() {
493 let report = create_test_report();
494 let mut dashboard = Dashboard::new(report);
495
496 dashboard.selected = 1;
497 assert_eq!(dashboard.selected, 1);
498 dashboard.selected = 100;
499 assert_eq!(dashboard.selected, 100);
500 }
501
502 #[test]
503 fn test_dashboard_report_summary_types() {
504 let report = StackHealthReport {
505 crates: vec![],
506 conflicts: vec![],
507 summary: HealthSummary {
508 total_crates: 0,
509 healthy_count: 0,
510 warning_count: 0,
511 error_count: 0,
512 path_dependency_count: 0,
513 conflict_count: 0,
514 },
515 timestamp: chrono::Utc::now(),
516 };
517
518 let dashboard = Dashboard::new(report);
519 assert_eq!(dashboard.report.summary.total_crates, 0);
520 assert!(dashboard.show_details);
521 }
522
523 #[test]
524 fn test_dashboard_with_unknown_status() {
525 let mut crates =
526 vec![CrateInfo::new("unknown", semver::Version::new(0, 1, 0), PathBuf::new())];
527 crates[0].status = CrateStatus::Unknown;
528
529 let report = StackHealthReport {
530 crates,
531 conflicts: vec![],
532 summary: HealthSummary::default(),
533 timestamp: chrono::Utc::now(),
534 };
535
536 let dashboard = Dashboard::new(report);
537 assert_eq!(dashboard.report.crates[0].status, CrateStatus::Unknown);
538 }
539
540 #[test]
541 fn test_dashboard_multiple_crates() {
542 let mut crates = vec![
543 CrateInfo::new("a", semver::Version::new(1, 0, 0), PathBuf::new()),
544 CrateInfo::new("b", semver::Version::new(2, 0, 0), PathBuf::new()),
545 CrateInfo::new("c", semver::Version::new(3, 0, 0), PathBuf::new()),
546 ];
547 for c in &mut crates {
548 c.status = CrateStatus::Healthy;
549 }
550
551 let report = StackHealthReport {
552 crates,
553 conflicts: vec![],
554 summary: HealthSummary { total_crates: 3, healthy_count: 3, ..Default::default() },
555 timestamp: chrono::Utc::now(),
556 };
557
558 let mut dashboard = Dashboard::new(report);
559
560 assert_eq!(dashboard.selected, 0);
561 dashboard.selected = 1;
562 assert_eq!(dashboard.selected, 1);
563 dashboard.selected = 2;
564 assert_eq!(dashboard.selected, 2);
565 }
566
567 #[test]
568 fn test_dashboard_crate_with_multiple_issues() {
569 let mut crates =
570 vec![CrateInfo::new("problematic", semver::Version::new(0, 1, 0), PathBuf::new())];
571 crates[0].status = CrateStatus::Warning;
572 crates[0].issues.push(CrateIssue::new(
573 IssueSeverity::Warning,
574 IssueType::VersionBehind,
575 "Version behind",
576 ));
577 crates[0].issues.push(CrateIssue::new(
578 IssueSeverity::Warning,
579 IssueType::VersionBehind,
580 "Another warning",
581 ));
582
583 let report = StackHealthReport {
584 crates,
585 conflicts: vec![],
586 summary: HealthSummary { total_crates: 1, warning_count: 1, ..Default::default() },
587 timestamp: chrono::Utc::now(),
588 };
589
590 let dashboard = Dashboard::new(report);
591 assert_eq!(dashboard.report.crates[0].issues.len(), 2);
592 }
593
594 mod render_tests {
595 use super::*;
596
597 #[test]
598 fn test_render_full_with_details() {
599 let report = create_test_report();
600 let mut dashboard = Dashboard::new(report);
601 dashboard.render();
602 assert!(dashboard.width > 0);
603 assert!(dashboard.height > 0);
604 }
605
606 #[test]
607 fn test_render_full_without_details() {
608 let report = create_test_report();
609 let mut dashboard = Dashboard::new(report);
610 dashboard.show_details = false;
611 dashboard.render();
612 assert!(dashboard.width > 0);
613 }
614
615 #[test]
616 fn test_render_with_all_status_types() {
617 let mut crates = vec![
618 CrateInfo::new("healthy", semver::Version::new(1, 0, 0), PathBuf::new()),
619 CrateInfo::new("warning", semver::Version::new(1, 0, 0), PathBuf::new()),
620 CrateInfo::new("error", semver::Version::new(1, 0, 0), PathBuf::new()),
621 CrateInfo::new("unknown", semver::Version::new(1, 0, 0), PathBuf::new()),
622 ];
623 crates[0].status = CrateStatus::Healthy;
624 crates[1].status = CrateStatus::Warning;
625 crates[2].status = CrateStatus::Error;
626 crates[3].status = CrateStatus::Unknown;
627
628 let report = StackHealthReport {
629 crates,
630 conflicts: vec![],
631 summary: HealthSummary {
632 total_crates: 4,
633 healthy_count: 1,
634 warning_count: 1,
635 error_count: 1,
636 ..Default::default()
637 },
638 timestamp: chrono::Utc::now(),
639 };
640
641 let mut dashboard = Dashboard::new(report);
642 dashboard.render();
643 assert!(dashboard.width > 0);
644 }
645
646 #[test]
647 fn test_render_table_with_selected() {
648 let mut crates = vec![
649 CrateInfo::new("first", semver::Version::new(1, 0, 0), PathBuf::new()),
650 CrateInfo::new("second", semver::Version::new(2, 0, 0), PathBuf::new()),
651 ];
652 crates[0].status = CrateStatus::Healthy;
653 crates[1].status = CrateStatus::Healthy;
654
655 let report = StackHealthReport {
656 crates,
657 conflicts: vec![],
658 summary: HealthSummary::default(),
659 timestamp: chrono::Utc::now(),
660 };
661
662 let mut dashboard = Dashboard::new(report);
663 dashboard.selected = 1;
664 dashboard.render();
665 assert!(dashboard.width > 0);
666 }
667
668 #[test]
669 fn test_render_empty_report() {
670 let report = StackHealthReport {
671 crates: vec![],
672 conflicts: vec![],
673 summary: HealthSummary::default(),
674 timestamp: chrono::Utc::now(),
675 };
676
677 let mut dashboard = Dashboard::new(report);
678 dashboard.render();
679 assert!(dashboard.width > 0);
680 }
681
682 #[test]
683 fn test_render_details_with_issues() {
684 let mut crates =
685 vec![CrateInfo::new("broken", semver::Version::new(0, 1, 0), PathBuf::new())];
686 crates[0].issues.push(CrateIssue::new(
687 IssueSeverity::Error,
688 IssueType::PathDependency,
689 "Test error message",
690 ));
691
692 let report = StackHealthReport {
693 crates,
694 conflicts: vec![],
695 summary: HealthSummary::default(),
696 timestamp: chrono::Utc::now(),
697 };
698
699 let mut dashboard = Dashboard::new(report);
700 dashboard.render();
701 assert!(dashboard.height > 0);
702 }
703
704 #[test]
709 fn test_write_str_basic() {
710 let report = create_test_report();
711 let mut dashboard = Dashboard::new(report);
712 dashboard.write_str(0, 0, "Hello", Color::WHITE);
714 assert!(dashboard.width > 5);
716 }
717
718 #[test]
719 fn test_write_str_overflow_width() {
720 let report = create_test_report();
721 let mut dashboard = Dashboard::new(report);
722 let long = "A".repeat(dashboard.width as usize + 20);
724 dashboard.write_str(0, 0, &long, Color::WHITE);
725 }
726
727 #[test]
728 fn test_write_str_at_edge() {
729 let report = create_test_report();
730 let mut dashboard = Dashboard::new(report);
731 let w = dashboard.width;
732 dashboard.write_str(w.saturating_sub(2), 0, "ABCD", Color::WHITE);
734 }
735
736 #[test]
737 fn test_set_char_within_bounds() {
738 let report = create_test_report();
739 let mut dashboard = Dashboard::new(report);
740 dashboard.set_char(0, 0, 'X', Color::RED);
741 dashboard.set_char(5, 5, 'Y', Color::GREEN);
742 }
743
744 #[test]
745 fn test_set_char_out_of_bounds() {
746 let report = create_test_report();
747 let mut dashboard = Dashboard::new(report);
748 let w = dashboard.width;
749 let h = dashboard.height;
750 dashboard.set_char(w, 0, 'Z', Color::WHITE);
752 dashboard.set_char(0, h, 'Z', Color::WHITE);
753 dashboard.set_char(w + 10, h + 10, 'Z', Color::WHITE);
754 }
755
756 #[test]
757 fn test_draw_box_minimal_size() {
758 let report = create_test_report();
759 let mut dashboard = Dashboard::new(report);
760 dashboard.draw_box(0, 0, 2, 2, "");
762 }
763
764 #[test]
765 fn test_draw_box_too_small() {
766 let report = create_test_report();
767 let mut dashboard = Dashboard::new(report);
768 dashboard.draw_box(0, 0, 1, 1, "");
770 dashboard.draw_box(0, 0, 0, 0, "");
771 dashboard.draw_box(0, 0, 1, 5, "");
772 dashboard.draw_box(0, 0, 5, 1, "");
773 }
774
775 #[test]
776 fn test_draw_box_with_title() {
777 let report = create_test_report();
778 let mut dashboard = Dashboard::new(report);
779 dashboard.draw_box(0, 0, 40, 10, " Title ");
780 }
781
782 #[test]
783 fn test_draw_box_title_too_wide() {
784 let report = create_test_report();
785 let mut dashboard = Dashboard::new(report);
786 dashboard.draw_box(0, 0, 5, 5, "Very long title text");
788 }
789
790 #[test]
791 fn test_render_title_errors_only() {
792 let report = StackHealthReport {
793 crates: vec![],
794 conflicts: vec![],
795 summary: HealthSummary {
796 total_crates: 1,
797 healthy_count: 0,
798 warning_count: 0,
799 error_count: 3,
800 path_dependency_count: 0,
801 conflict_count: 0,
802 },
803 timestamp: chrono::Utc::now(),
804 };
805 let mut dashboard = Dashboard::new(report);
806 dashboard.render_title(0, 0, dashboard.width, 3);
807 }
808
809 #[test]
810 fn test_render_title_warnings_only() {
811 let report = StackHealthReport {
812 crates: vec![],
813 conflicts: vec![],
814 summary: HealthSummary {
815 total_crates: 2,
816 healthy_count: 1,
817 warning_count: 2,
818 error_count: 0,
819 path_dependency_count: 0,
820 conflict_count: 0,
821 },
822 timestamp: chrono::Utc::now(),
823 };
824 let mut dashboard = Dashboard::new(report);
825 dashboard.render_title(0, 0, dashboard.width, 3);
826 }
827
828 #[test]
829 fn test_render_title_all_healthy() {
830 let report = StackHealthReport {
831 crates: vec![],
832 conflicts: vec![],
833 summary: HealthSummary {
834 total_crates: 5,
835 healthy_count: 5,
836 warning_count: 0,
837 error_count: 0,
838 path_dependency_count: 0,
839 conflict_count: 0,
840 },
841 timestamp: chrono::Utc::now(),
842 };
843 let mut dashboard = Dashboard::new(report);
844 dashboard.render_title(0, 0, dashboard.width, 3);
845 }
846
847 #[test]
848 fn test_render_table_no_crates_io_version() {
849 let mut crates =
850 vec![CrateInfo::new("local_only", semver::Version::new(0, 1, 0), PathBuf::new())];
851 crates[0].status = CrateStatus::Healthy;
852 let report = StackHealthReport {
855 crates,
856 conflicts: vec![],
857 summary: HealthSummary::default(),
858 timestamp: chrono::Utc::now(),
859 };
860
861 let mut dashboard = Dashboard::new(report);
862 let w = dashboard.width;
863 dashboard.render_table(0, 3, w, 15);
864 }
865
866 #[test]
867 fn test_render_table_with_issues_count() {
868 let mut crates =
869 vec![CrateInfo::new("buggy", semver::Version::new(1, 0, 0), PathBuf::new())];
870 crates[0].status = CrateStatus::Error;
871 crates[0].issues.push(CrateIssue::new(
872 IssueSeverity::Error,
873 IssueType::PathDependency,
874 "issue 1",
875 ));
876 crates[0].issues.push(CrateIssue::new(
877 IssueSeverity::Warning,
878 IssueType::VersionBehind,
879 "issue 2",
880 ));
881
882 let report = StackHealthReport {
883 crates,
884 conflicts: vec![],
885 summary: HealthSummary::default(),
886 timestamp: chrono::Utc::now(),
887 };
888
889 let mut dashboard = Dashboard::new(report);
890 let w = dashboard.width;
891 dashboard.render_table(0, 3, w, 15);
892 }
893
894 #[test]
895 fn test_render_details_no_issues() {
896 let mut crates =
897 vec![CrateInfo::new("clean", semver::Version::new(2, 0, 0), PathBuf::new())];
898 crates[0].status = CrateStatus::Healthy;
899
900 let report = StackHealthReport {
901 crates,
902 conflicts: vec![],
903 summary: HealthSummary::default(),
904 timestamp: chrono::Utc::now(),
905 };
906
907 let mut dashboard = Dashboard::new(report);
908 let w = dashboard.width;
909 dashboard.render_details(0, 10, w, 8);
910 }
911
912 #[test]
913 fn test_render_details_no_crate_selected() {
914 let report = StackHealthReport {
916 crates: vec![],
917 conflicts: vec![],
918 summary: HealthSummary::default(),
919 timestamp: chrono::Utc::now(),
920 };
921
922 let mut dashboard = Dashboard::new(report);
923 let w = dashboard.width;
924 dashboard.render_details(0, 10, w, 8);
925 }
926
927 #[test]
928 fn test_render_details_selected_out_of_range() {
929 let mut crates =
930 vec![CrateInfo::new("only_one", semver::Version::new(1, 0, 0), PathBuf::new())];
931 crates[0].status = CrateStatus::Healthy;
932
933 let report = StackHealthReport {
934 crates,
935 conflicts: vec![],
936 summary: HealthSummary::default(),
937 timestamp: chrono::Utc::now(),
938 };
939
940 let mut dashboard = Dashboard::new(report);
941 dashboard.selected = 99; let w = dashboard.width;
943 dashboard.render_details(0, 10, w, 8);
944 }
945
946 #[test]
947 fn test_render_details_multiple_issues_truncated() {
948 let mut crates =
949 vec![CrateInfo::new("many_issues", semver::Version::new(0, 1, 0), PathBuf::new())];
950 crates[0].status = CrateStatus::Error;
951 for i in 0..10 {
952 crates[0].issues.push(CrateIssue::new(
953 IssueSeverity::Error,
954 IssueType::PathDependency,
955 format!("Error number {}", i),
956 ));
957 }
958
959 let report = StackHealthReport {
960 crates,
961 conflicts: vec![],
962 summary: HealthSummary::default(),
963 timestamp: chrono::Utc::now(),
964 };
965
966 let mut dashboard = Dashboard::new(report);
967 let w = dashboard.width;
968 dashboard.render_details(0, 10, w, 6);
970 }
971
972 #[test]
973 fn test_render_help_standalone() {
974 let report = create_test_report();
975 let mut dashboard = Dashboard::new(report);
976 let w = dashboard.width;
977 let h = dashboard.height;
978 dashboard.render_help(0, h.saturating_sub(3), w, 3);
979 }
980
981 #[test]
982 fn test_render_with_small_buffer() {
983 let report = create_test_report();
984 let mut dashboard = Dashboard::new(report);
985 dashboard.width = 20;
987 dashboard.height = 10;
988 dashboard.buffer = CellBuffer::new(20, 10);
989 dashboard.render();
990 }
991
992 #[test]
993 fn test_render_long_crate_name_truncation() {
994 let mut crates = vec![CrateInfo::new(
995 "a_very_long_crate_name_that_exceeds_column",
996 semver::Version::new(1, 0, 0),
997 PathBuf::new(),
998 )];
999 crates[0].status = CrateStatus::Healthy;
1000 crates[0].crates_io_version = Some(semver::Version::new(1, 0, 0));
1001
1002 let report = StackHealthReport {
1003 crates,
1004 conflicts: vec![],
1005 summary: HealthSummary::default(),
1006 timestamp: chrono::Utc::now(),
1007 };
1008
1009 let mut dashboard = Dashboard::new(report);
1010 dashboard.render();
1011 }
1012 }
1013}