use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::{ViewApp, ViewTab, render};
use crate::tui::test_support::{
AIBOM_BSI, DEMO_NEW, SIZES, aibom_single, demo_single, pin_theme, render_to_text,
};
use crate::tui::view::events::handle_key_event;
fn demo_view_app(active_tab: ViewTab) -> ViewApp {
pin_theme();
let (sbom, profile) = demo_single();
let mut app = ViewApp::new(sbom, DEMO_NEW, profile);
app.active_tab = active_tab;
app
}
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
fn render_tab(active_tab: ViewTab, width: u16, height: u16) -> String {
let mut app = demo_view_app(active_tab);
render_to_text(width, height, |frame| {
render(frame, &mut app);
})
}
const VIEW_TABS: [(&str, ViewTab); 8] = [
("overview", ViewTab::Overview),
("tree", ViewTab::Tree),
("vulnerabilities", ViewTab::Vulnerabilities),
("licenses", ViewTab::Licenses),
("dependencies", ViewTab::Dependencies),
("quality", ViewTab::Quality),
("compliance", ViewTab::Compliance),
("source", ViewTab::Source),
];
#[test]
fn snapshot_all_view_tabs() {
let mut settings = insta::Settings::clone_current();
settings.add_filter(
r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+\([^│\n]*",
"$1 (AGE)",
);
settings.add_filter(r"Age: \d+d", "Age: [N]d");
settings.bind(|| {
for (name, tab) in VIEW_TABS {
for (w, h) in SIZES {
let text = render_tab(tab, w, h);
insta::assert_snapshot!(format!("view_{name}_{w}x{h}"), text);
}
}
});
}
#[test]
fn tab_switch_cycles_within_profile() {
let mut app = demo_view_app(ViewTab::Overview);
handle_key_event(&mut app, key(KeyCode::Tab));
assert_eq!(app.active_tab, ViewTab::Tree);
handle_key_event(&mut app, key(KeyCode::Tab));
assert_eq!(app.active_tab, ViewTab::Vulnerabilities);
handle_key_event(&mut app, KeyEvent::new(KeyCode::Tab, KeyModifiers::SHIFT));
assert_eq!(app.active_tab, ViewTab::Tree);
}
#[test]
fn numeric_keys_jump_to_view_tab() {
let mut app = demo_view_app(ViewTab::Overview);
handle_key_event(&mut app, key(KeyCode::Char('3')));
assert_eq!(app.active_tab, ViewTab::Vulnerabilities);
handle_key_event(&mut app, key(KeyCode::Char('1')));
assert_eq!(app.active_tab, ViewTab::Overview);
}
#[test]
fn help_overlay_toggles() {
let mut app = demo_view_app(ViewTab::Overview);
handle_key_event(&mut app, key(KeyCode::Char('?')));
assert!(app.show_help);
handle_key_event(&mut app, key(KeyCode::Char('?')));
assert!(!app.show_help);
}
#[test]
fn tree_search_entry_activates_filter() {
let mut app = demo_view_app(ViewTab::Tree);
assert!(!app.tree_search_active);
handle_key_event(&mut app, key(KeyCode::Char('/')));
assert!(
app.tree_search_active,
"'/' on the Tree tab starts the inline tree filter"
);
}
fn aibom_view_app(active_tab: ViewTab) -> ViewApp {
pin_theme();
let (sbom, profile) = aibom_single();
let mut app = ViewApp::new(sbom, AIBOM_BSI, profile);
app.active_tab = active_tab;
app
}
#[test]
fn aibom_fixture_detected_as_aibom_profile() {
let (_sbom, profile) = aibom_single();
assert_eq!(profile, crate::model::BomProfile::AiBom);
}
#[test]
fn aibom_view_app_uses_ai_readiness_scoring() {
let app = aibom_view_app(ViewTab::AiReadiness);
assert_eq!(
app.quality_report.profile,
crate::quality::ScoringProfile::AiReadiness
);
}
#[test]
fn aibom_profile_exposes_ai_tab_suite() {
let tabs = ViewTab::tabs_for_profile(crate::model::BomProfile::AiBom);
assert!(tabs.contains(&ViewTab::Models));
assert!(tabs.contains(&ViewTab::Datasets));
assert!(tabs.contains(&ViewTab::AiReadiness));
}
const AIBOM_TABS: [(&str, ViewTab); 3] = [
("models", ViewTab::Models),
("datasets", ViewTab::Datasets),
("ai_readiness", ViewTab::AiReadiness),
];
#[test]
fn snapshot_aibom_tabs() {
let mut settings = insta::Settings::clone_current();
settings.add_filter(r"Age: \d+d", "Age: [N]d");
settings.bind(|| {
for (name, tab) in AIBOM_TABS {
let mut app = aibom_view_app(tab);
let text = render_to_text(120, 40, |frame| {
render(frame, &mut app);
});
insta::assert_snapshot!(format!("aibom_{name}_120x40"), text);
}
});
}
use crate::model::{
CanonicalId, Component, NormalizedSbom, Severity, VulnerabilityRef, VulnerabilitySource,
};
fn epss_kev_view_app() -> ViewApp {
pin_theme();
let mut sbom = NormalizedSbom::default();
let mut comp = Component::new("openssl".to_string(), "openssl-ref".to_string())
.with_version("3.0.0".to_string());
let mut vuln = VulnerabilityRef::new("CVE-2024-9999".to_string(), VulnerabilitySource::Nvd);
vuln.severity = Some(Severity::Critical);
vuln.is_kev = true;
vuln.epss_score = Some(0.84);
comp.vulnerabilities.push(vuln);
sbom.components
.insert(CanonicalId::from_name_version("openssl", None), comp);
let mut app = ViewApp::new(sbom, "", crate::model::BomProfile::Sbom);
app.active_tab = ViewTab::Vulnerabilities;
app
}
#[test]
fn vuln_explorer_renders_epss_badge() {
let mut app = epss_kev_view_app();
let text = render_to_text(120, 40, |frame| {
render(frame, &mut app);
});
assert!(
text.contains("EPSS 84%"),
"EPSS badge should render in the view-mode vuln explorer:\n{text}"
);
}
#[test]
fn kev_filter_key_toggles_and_renders_state() {
let mut app = epss_kev_view_app();
assert!(!app.vuln_state.filter_kev);
handle_key_event(&mut app, key(KeyCode::Char('k')));
assert!(app.vuln_state.filter_kev, "'k' enables the KEV-only filter");
let text = render_to_text(120, 40, |frame| {
render(frame, &mut app);
});
assert!(text.contains("CVE-2024-9999"), "KEV vuln survives filter");
assert!(text.contains("KEV:"), "filter bar shows KEV state");
assert!(
text.contains("[k]"),
"footer hint advertises the [k] toggle"
);
handle_key_event(&mut app, key(KeyCode::Char('k')));
assert!(
!app.vuln_state.filter_kev,
"'k' toggles the filter back off"
);
}
#[test]
fn compliance_selector_exposes_every_standard() {
use crate::quality::ComplianceLevel;
let levels = ComplianceLevel::all();
for (idx, level) in levels.iter().enumerate() {
let mut app = aibom_view_app(ViewTab::Compliance);
app.ensure_compliance_results();
app.compliance_state.selected_standard = idx;
let text = render_to_text(120, 40, |frame| {
render(frame, &mut app);
});
let label = level.short_name();
assert!(
text.contains(label),
"compliance standard `{label}` (index {idx}) must scroll into view:\n{text}"
);
}
assert!(levels.iter().any(|l| l.short_name() == "AI-Act"));
assert!(levels.iter().any(|l| l.short_name() == "BSI-AI"));
}