use super::super::*;
use super::support::*;
#[test]
fn alt_a_works_with_help_but_modal_precedence_blocks_it() {
let mut state = state_with_activity();
state.show_help = true;
handle_key(
modified_key(KeyCode::Char('a'), KeyModifiers::ALT),
&mut state,
);
assert!(!state.activity_column_visible());
assert!(state.show_help);
state.open_system_prompt_modal("modal content".to_string());
handle_key(
modified_key(KeyCode::Char('a'), KeyModifiers::ALT),
&mut state,
);
assert!(!state.activity_column_visible());
assert!(state.system_prompt_modal_visible());
}
#[test]
fn system_prompt_modal_alt_arrows_scroll_modal_and_suppress_underlying_state() {
let mut state = state_with_activity();
state.focus_prompt();
state.set_prompt_text("line one\nline two", "line one\nline two".len());
state.set_scroll_offset(&state.scroll_views.transcript, 2);
state.set_scroll_offset(&state.scroll_views.activity_tree, 3);
state.set_scroll_offset(&state.scroll_views.detail, 4);
state.open_system_prompt_modal(
(0..12)
.map(|line| format!("modal line {line}"))
.collect::<Vec<_>>()
.join("\n"),
);
let viewport = Some(SystemPromptViewport {
visible_rows: 3,
wrap_width: 80,
});
assert!(matches!(
handle_key_with_all_viewports(
modified_key(KeyCode::Down, KeyModifiers::ALT),
&mut state,
KeyViewports {
system_prompt: viewport,
..KeyViewports::default()
},
&[]
),
InputAction::None
));
assert_eq!(state.scroll_offset(&state.scroll_views.system_prompt), 1);
assert_eq!(state.prompt_cursor(), "line one\nline two".len());
assert_eq!(state.transcript_scroll_offset(), 2);
assert_eq!(state.activity_scroll_offset(), 3);
assert_eq!(state.detail_offset(), 4);
assert!(matches!(
handle_key_with_all_viewports(
modified_key(KeyCode::Up, KeyModifiers::ALT),
&mut state,
KeyViewports {
system_prompt: viewport,
..KeyViewports::default()
},
&[]
),
InputAction::None
));
assert_eq!(state.scroll_offset(&state.scroll_views.system_prompt), 0);
assert_eq!(state.prompt_cursor(), "line one\nline two".len());
assert_eq!(state.transcript_scroll_offset(), 2);
assert_eq!(state.activity_scroll_offset(), 3);
assert_eq!(state.detail_offset(), 4);
}
fn theme_picker_state() -> MissionControlState {
use crate::appearance::ThemeCatalogEntry;
use opaline::ThemeVariant::{Dark, Light};
let entries = [
("dark-one", "Shared dark", Dark),
("light-one", "Shared light", Light),
("dark-two", "Other dark", Dark),
("light-two", "Other light", Light),
]
.into_iter()
.map(|(id, display_name, variant)| ThemeCatalogEntry {
id: id.into(),
display_name: display_name.into(),
variant,
})
.collect();
let mut state = MissionControlState::default();
state.open_theme_picker("dark-one".into(), 1, false, None, entries, None);
state
}
fn apply_theme_picker_key(key: KeyEvent, state: &mut MissionControlState) -> (InputAction, bool) {
let command = classify_key_at(
key,
state,
KeyViewports {
theme_picker: Some(PickerViewport { visible_rows: 2 }),
theme_query_visible: true,
..Default::default()
},
&[],
Instant::now(),
);
let (action, display) = command.into_parts();
let changed = display.is_some_and(|command| {
crate::tui::controller::apply_display_command_for_test(command, state, &[]).0
});
if let InputAction::PreviewTheme(id) = &action {
assert_eq!(
state.theme_picker_selected_id().as_deref(),
Some(id.as_str())
);
}
(action, changed)
}
#[test]
fn theme_picker_tab_search_and_navigation_preview_the_selected_category() {
let mut state = theme_picker_state();
for (code, expected) in [
(KeyCode::Down, "dark-two"),
(KeyCode::Up, "dark-one"),
(KeyCode::PageDown, "dark-two"),
(KeyCode::PageUp, "dark-one"),
] {
assert!(
matches!(apply_theme_picker_key(key(code), &mut state).0, InputAction::PreviewTheme(id) if id == expected)
);
}
for ch in "shared".chars() {
assert!(
matches!(apply_theme_picker_key(key(KeyCode::Char(ch)), &mut state).0, InputAction::PreviewTheme(id) if id == "dark-one")
);
}
for (event, expected) in [
(key(KeyCode::Tab), "light-one"),
(
modified_key(KeyCode::BackTab, KeyModifiers::SHIFT),
"dark-one",
),
(modified_key(KeyCode::Tab, KeyModifiers::SHIFT), "light-one"),
(key(KeyCode::BackTab), "dark-one"),
] {
assert!(
matches!(apply_theme_picker_key(event, &mut state).0, InputAction::PreviewTheme(id) if id == expected)
);
let picker = state.modals.theme_picker.as_ref().unwrap();
assert_eq!(picker.query, "shared");
assert_eq!(picker.query_cursor, 6);
assert_eq!(picker.filtered_indices.len(), 1);
}
}
#[test]
fn theme_picker_empty_results_redraw_without_preview_or_save_and_keep_cursor_keys() {
let mut state = theme_picker_state();
for ch in "dark".chars() {
apply_theme_picker_key(key(KeyCode::Char(ch)), &mut state);
}
let (action, changed) = apply_theme_picker_key(key(KeyCode::Tab), &mut state);
assert!(changed);
assert!(matches!(action, InputAction::None));
assert!(state.theme_picker_selected_id().is_none());
assert_eq!(state.theme_picker_preview_id(), Some("dark-one"));
assert!(matches!(
apply_theme_picker_key(key(KeyCode::Enter), &mut state).0,
InputAction::None
));
for (code, cursor) in [
(KeyCode::Home, 0),
(KeyCode::Right, 1),
(KeyCode::End, 4),
(KeyCode::Left, 3),
] {
apply_theme_picker_key(key(code), &mut state);
let picker = state.modals.theme_picker.as_ref().unwrap();
assert_eq!(picker.query_cursor, cursor);
assert_eq!(picker.active_variant, opaline::ThemeVariant::Light);
}
for code in [KeyCode::Char('界'), KeyCode::Backspace, KeyCode::Delete] {
let (action, changed) = apply_theme_picker_key(key(code), &mut state);
assert!(
changed,
"{code:?} must repaint the search even with no selection"
);
assert!(matches!(action, InputAction::None));
assert_eq!(state.theme_picker_preview_id(), Some("dark-one"));
}
assert_eq!(state.modals.theme_picker.as_ref().unwrap().query, "dar");
assert!(
matches!(apply_theme_picker_key(key(KeyCode::Tab), &mut state).0, InputAction::PreviewTheme(id) if id == "dark-one")
);
assert!(matches!(
apply_theme_picker_key(key(KeyCode::Enter), &mut state).0,
InputAction::ConfirmTheme
));
assert!(matches!(
apply_theme_picker_key(key(KeyCode::Esc), &mut state).0,
InputAction::CancelTheme
));
}
#[test]
fn theme_picker_loading_and_pending_save_block_tab_changes() {
let mut state = theme_picker_state();
state.set_theme_picker_save_pending();
let before = state.modals.theme_picker.clone();
for code in [
KeyCode::Tab,
KeyCode::BackTab,
KeyCode::Down,
KeyCode::Char('x'),
KeyCode::Enter,
KeyCode::Esc,
] {
assert!(matches!(
apply_theme_picker_key(key(code), &mut state).0,
InputAction::None
));
assert_eq!(state.modals.theme_picker, before);
}
state.set_theme_picker_save_failed("disk full".into());
assert!(
matches!(apply_theme_picker_key(key(KeyCode::Tab), &mut state).0, InputAction::PreviewTheme(id) if id == "light-one")
);
state.open_theme_picker("light-one".into(), 1, true, Some(2), Vec::new(), None);
for code in [
KeyCode::Tab,
KeyCode::BackTab,
KeyCode::Char('x'),
KeyCode::Down,
KeyCode::Enter,
] {
assert!(matches!(
apply_theme_picker_key(key(code), &mut state).0,
InputAction::None
));
let picker = state.modals.theme_picker.as_ref().unwrap();
assert!(picker.query.is_empty());
assert_eq!(picker.active_variant, opaline::ThemeVariant::Dark);
assert!(picker.loading);
}
assert!(matches!(
apply_theme_picker_key(key(KeyCode::Esc), &mut state).0,
InputAction::CancelTheme
));
}