mod support;
use holodeck_core::models::InstalledApp;
use holodeck_simctl_tui::Theme;
use holodeck_simctl_tui::state::{AppState, CommandPalette, CreateWizard, LaunchAppPrompt, Modal, OpenUrlPrompt, PrivacyWizard};
use holodeck_simctl_tui::view::render;
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use ratatui::style::Color;
use support::{booted, shutdown, state_with};
fn rendered(state: &AppState) -> String {
rendered_with(state, &Theme::default_plus())
}
fn rendered_with(state: &AppState, theme: &Theme) -> String {
let (text, _) = render_to_text_and_buffer(state, theme);
text
}
fn render_to_text_and_buffer(state: &AppState, theme: &Theme) -> (String, ratatui::buffer::Buffer) {
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal.draw(|frame| render(frame, state, theme)).unwrap();
let buffer = terminal.backend().buffer().clone();
let area = buffer.area;
let mut out = String::new();
for y in 0..area.height {
for x in 0..area.width {
out.push_str(buffer[(x, y)].symbol());
}
out.push('\n');
}
(out, buffer)
}
#[test]
fn main_list_shows_header_and_simulators() {
let state = state_with(vec![booted("iPhone 17 Pro"), shutdown("iPhone 12")]);
let text = rendered(&state);
assert!(text.contains("holodeck"));
assert!(text.contains("iPhone 17 Pro"));
assert!(text.contains("iPhone 12"));
assert!(text.contains("Booted"));
assert!(text.contains("Shutdown"));
}
#[test]
fn empty_simulator_list_shows_placeholder() {
let state = state_with(vec![]);
let text = rendered(&state);
assert!(text.contains("no simulators"));
}
#[test]
fn filter_banner_appears_when_filter_is_focused() {
let mut state = state_with(vec![booted("A")]);
state.is_filter_focused = true;
state.filter_query = "iPh".to_string();
let text = rendered(&state);
assert!(text.contains("Filter: iPh"));
}
#[test]
fn recording_banner_shows_while_recording() {
let mut state = state_with(vec![booted("A")]);
state.recording_device_id = Some(state.simulators[0].id);
let text = rendered(&state);
assert!(text.contains("Recording"));
}
#[test]
fn status_bar_shows_error_over_status_message() {
let mut state = state_with(vec![]);
state.status_message = Some("Refreshing…".to_string());
state.last_error = Some("boom".to_string());
let text = rendered(&state);
assert!(text.contains("boom"));
}
#[test]
fn appearance_modal_banner_renders() {
let mut state = state_with(vec![booted("A")]);
state.modal = Some(Modal::Appearance(0));
let text = rendered(&state);
assert!(text.contains("Appearance"));
assert!(text.contains("Light"));
assert!(text.contains("Dark"));
}
#[test]
fn confirm_erase_banner_renders() {
let mut state = state_with(vec![shutdown("A")]);
let id = state.simulators[0].id;
state.modal = Some(Modal::ConfirmErase(id, 1));
let text = rendered(&state);
assert!(text.contains("Erase"));
}
#[test]
fn help_overlay_lists_keybindings() {
let mut state = state_with(vec![]);
state.modal = Some(Modal::Help);
let text = rendered(&state);
assert!(text.contains("Keybindings"));
assert!(text.contains("Navigate the simulator list"));
assert!(text.contains("Quit"));
}
#[test]
fn inspector_shows_simulator_details() {
let mut state = state_with(vec![booted("A")]);
let id = state.simulators[0].id;
state.modal = Some(Modal::Inspector(id));
let text = rendered(&state);
assert!(text.contains("Inspector"));
assert!(text.contains(&id.to_string().to_uppercase()));
assert!(text.contains("Booted"));
}
#[test]
fn open_url_prompt_shows_typed_url_and_error() {
let mut prompt = OpenUrlPrompt::new(uuid::Uuid::new_v4());
prompt.url = "https://apple.com".to_string();
prompt.error = Some("bad url".to_string());
let mut state = state_with(vec![]);
state.modal = Some(Modal::OpenUrl(prompt));
let text = rendered(&state);
assert!(text.contains("https://apple.com"));
assert!(text.contains("bad url"));
}
#[test]
fn create_wizard_confirm_step_shows_selection() {
use holodeck_core::models::{DeviceType, Runtime};
let mut wizard = CreateWizard::new();
wizard.step = holodeck_simctl_tui::state::CreateWizardStep::Confirm;
wizard.device_types = vec![DeviceType::new("id", "iPhone 17 Pro")];
wizard.runtimes = vec![Runtime::from_identifier("com.apple.CoreSimulator.SimRuntime.iOS-18-0").unwrap()];
let mut state = state_with(vec![]);
state.modal = Some(Modal::CreateWizard(wizard));
let text = rendered(&state);
assert!(text.contains("iPhone 17 Pro"));
assert!(text.contains("iOS 18.0"));
}
#[test]
fn privacy_wizard_pick_app_lists_apps() {
let mut wizard = PrivacyWizard::new(uuid::Uuid::new_v4());
wizard.step = holodeck_simctl_tui::state::PrivacyWizardStep::PickApp;
wizard.all_apps = vec![InstalledApp::new("com.example.a", "Alpha", None, true)];
let mut state = state_with(vec![]);
state.modal = Some(Modal::PrivacyWizard(wizard));
let text = rendered(&state);
assert!(text.contains("Alpha"));
assert!(text.contains("com.example.a"));
}
#[test]
fn launch_modal_pick_app_lists_apps() {
let mut prompt = LaunchAppPrompt::new(uuid::Uuid::new_v4());
prompt.step = holodeck_simctl_tui::state::LaunchAppStep::PickApp;
prompt.all_apps = vec![InstalledApp::new("com.example.a", "Alpha", None, true)];
let mut state = state_with(vec![]);
state.modal = Some(Modal::LaunchApp(prompt));
let text = rendered(&state);
assert!(text.contains("Alpha"));
assert!(text.contains("com.example.a"));
}
#[test]
fn launch_modal_pick_language_lists_options() {
let mut prompt = LaunchAppPrompt::new(uuid::Uuid::new_v4());
prompt.step = holodeck_simctl_tui::state::LaunchAppStep::PickLanguage;
let mut state = state_with(vec![]);
state.modal = Some(Modal::LaunchApp(prompt));
let text = rendered(&state);
assert!(text.contains("Portuguese (Brazil)"));
}
#[test]
fn launch_modal_shows_the_filter_banner_when_focused() {
let mut prompt = LaunchAppPrompt::new(uuid::Uuid::new_v4());
prompt.step = holodeck_simctl_tui::state::LaunchAppStep::PickLanguage;
prompt.is_language_filter_focused = true;
prompt.language_filter = "braz".to_string();
let mut state = state_with(vec![]);
state.modal = Some(Modal::LaunchApp(prompt));
let text = rendered(&state);
assert!(text.contains("Filter:"));
assert!(text.contains("braz"));
assert!(text.contains("Portuguese (Brazil)"));
}
#[test]
fn launch_modal_pick_region_lists_options() {
let mut prompt = LaunchAppPrompt::new(uuid::Uuid::new_v4());
prompt.step = holodeck_simctl_tui::state::LaunchAppStep::PickRegion;
let mut state = state_with(vec![]);
state.modal = Some(Modal::LaunchApp(prompt));
let text = rendered(&state);
assert!(text.contains("Brazil"));
}
#[test]
fn launch_modal_region_breadcrumb_shows_the_chained_language() {
let mut prompt = LaunchAppPrompt::new(uuid::Uuid::new_v4());
prompt.step = holodeck_simctl_tui::state::LaunchAppStep::PickRegion;
prompt.chosen_language = Some(&holodeck_core::models::LanguageOption::ALL[0]);
let mut state = state_with(vec![]);
state.modal = Some(Modal::LaunchApp(prompt));
let text = rendered(&state);
assert!(text.contains("pick a region"));
assert!(text.contains(holodeck_core::models::LanguageOption::ALL[0].display_name));
}
#[test]
fn launch_modal_renders_the_launch_failure() {
let mut prompt = LaunchAppPrompt::new(uuid::Uuid::new_v4());
prompt.step = holodeck_simctl_tui::state::LaunchAppStep::PickApp;
prompt.error = Some("boom".to_string());
let mut state = state_with(vec![]);
state.modal = Some(Modal::LaunchApp(prompt));
let text = rendered(&state);
assert!(text.contains("boom"));
}
#[test]
fn command_palette_overlay_shows_query_and_ghost_suffix() {
let mut state = state_with(vec![shutdown("A")]);
state.modal =
Some(Modal::CommandPalette(CommandPalette { simulator_id: Some(state.simulators[0].id), query: "bo".to_string() }));
let text = rendered(&state);
assert!(text.contains("Command palette"));
assert!(text.contains("bo"));
}
#[test]
fn booted_and_shutdown_dots_use_success_and_hint_colors() {
let state = state_with(vec![booted("A"), booted("B"), shutdown("C")]);
let theme = Theme::default_plus();
let (_, buffer) = render_to_text_and_buffer(&state, &theme);
let dots: Vec<_> = buffer.content().iter().filter(|cell| cell.symbol() == "●" || cell.symbol() == "○").collect();
assert_eq!(dots.len(), 3, "expected two booted dots and one shutdown dot");
assert!(
dots.iter().any(|cell| cell.symbol() == "●" && cell.fg == theme.success),
"an unselected booted dot should be success-colored"
);
assert!(dots.iter().any(|cell| cell.symbol() == "○" && cell.fg == theme.muted_text), "the shutdown dot should be muted");
}
#[test]
fn status_bar_uses_error_color_over_bar_background() {
let mut state = state_with(vec![]);
state.last_error = Some("boom".to_string());
let theme = Theme::default_plus();
let (_, buffer) = render_to_text_and_buffer(&state, &theme);
let cell = &buffer[(1, 23)]; assert_eq!(cell.fg, theme.error);
assert_eq!(cell.bg, Color::Reset);
}
#[test]
fn switching_to_the_ansi_theme_changes_rendered_colors() {
let state = state_with(vec![booted("A"), shutdown("B")]);
let (default_plus_text, default_plus_buffer) = render_to_text_and_buffer(&state, &Theme::default_plus());
let (ansi_text, ansi_buffer) = render_to_text_and_buffer(&state, &Theme::ansi());
assert_eq!(default_plus_text, ansi_text, "content should be identical — only styling should differ");
assert_ne!(default_plus_buffer, ansi_buffer, "the two themes should produce visibly different styling");
}