use super::should_spawn_warm_server;
use super::helpers::{combined_data_version, list_windows_json_with_tabs};
use crate::types::AppState;
#[test]
fn set_hook_replaces_existing_hook() {
let mut app = AppState::new("test".to_string());
crate::config::parse_config_line(&mut app, "set-hook -g client-attached 'display-message first'");
crate::config::parse_config_line(&mut app, "set-hook -g client-attached 'display-message second'");
let cmds = app.hooks.get("client-attached").unwrap();
assert_eq!(cmds.len(), 1, "hook should be replaced, not appended");
assert_eq!(cmds[0], "display-message second");
}
#[test]
fn set_hook_unset_removes_hook() {
let mut app = AppState::new("test".to_string());
crate::config::parse_config_line(&mut app, "set-hook -g client-attached 'display-message hello'");
assert!(app.hooks.contains_key("client-attached"));
crate::config::parse_config_line(&mut app, "set-hook -gu client-attached");
assert!(!app.hooks.contains_key("client-attached"), "hook should be removed by -gu");
}
#[test]
fn set_hook_different_hooks_coexist() {
let mut app = AppState::new("test".to_string());
crate::config::parse_config_line(&mut app, "set-hook -g client-attached 'display-message a'");
crate::config::parse_config_line(&mut app, "set-hook -g after-new-window 'display-message b'");
assert_eq!(app.hooks.len(), 2);
assert_eq!(app.hooks["client-attached"][0], "display-message a");
assert_eq!(app.hooks["after-new-window"][0], "display-message b");
}
#[test]
fn set_hook_replace_preserves_other_hooks() {
let mut app = AppState::new("test".to_string());
crate::config::parse_config_line(&mut app, "set-hook -g client-attached 'cmd-a'");
crate::config::parse_config_line(&mut app, "set-hook -g after-new-window 'cmd-b'");
crate::config::parse_config_line(&mut app, "set-hook -g client-attached 'cmd-c'");
assert_eq!(app.hooks["client-attached"], vec!["cmd-c"]);
assert_eq!(app.hooks["after-new-window"], vec!["cmd-b"]);
}
#[test]
fn set_hook_unset_with_u_flag() {
let mut app = AppState::new("test".to_string());
crate::config::parse_config_line(&mut app, "set-hook -g client-attached 'hello'");
crate::config::parse_config_line(&mut app, "set-hook -u client-attached");
assert!(!app.hooks.contains_key("client-attached"), "hook should be removed by -u");
}
#[test]
fn set_hook_ga_appends_to_existing() {
let mut app = AppState::new("test".to_string());
crate::config::parse_config_line(&mut app, "set-hook -g client-attached 'display-message first'");
crate::config::parse_config_line(&mut app, "set-hook -ga client-attached 'display-message second'");
let cmds = app.hooks.get("client-attached").unwrap();
assert_eq!(cmds.len(), 2, "-ga should append, giving 2 handlers");
assert_eq!(cmds[0], "display-message first");
assert_eq!(cmds[1], "display-message second");
}
#[test]
fn set_hook_ga_creates_if_missing() {
let mut app = AppState::new("test".to_string());
crate::config::parse_config_line(&mut app, "set-hook -ga client-attached 'display-message only'");
let cmds = app.hooks.get("client-attached").unwrap();
assert_eq!(cmds.len(), 1, "-ga on missing hook should create it");
assert_eq!(cmds[0], "display-message only");
}
#[test]
fn set_hook_g_replaces_appended_hooks() {
let mut app = AppState::new("test".to_string());
crate::config::parse_config_line(&mut app, "set-hook -g client-attached 'cmd-a'");
crate::config::parse_config_line(&mut app, "set-hook -ga client-attached 'cmd-b'");
crate::config::parse_config_line(&mut app, "set-hook -ga client-attached 'cmd-c'");
assert_eq!(app.hooks["client-attached"].len(), 3);
crate::config::parse_config_line(&mut app, "set-hook -g client-attached 'cmd-new'");
let cmds = app.hooks.get("client-attached").unwrap();
assert_eq!(cmds.len(), 1, "-g should replace entire list");
assert_eq!(cmds[0], "cmd-new");
}
#[test]
fn set_hook_gu_removes_all_appended_hooks() {
let mut app = AppState::new("test".to_string());
crate::config::parse_config_line(&mut app, "set-hook -g client-attached 'cmd-a'");
crate::config::parse_config_line(&mut app, "set-hook -ga client-attached 'cmd-b'");
assert_eq!(app.hooks["client-attached"].len(), 2);
crate::config::parse_config_line(&mut app, "set-hook -gu client-attached");
assert!(!app.hooks.contains_key("client-attached"), "-gu should remove all handlers");
}
#[test]
fn set_hook_a_flag_without_g() {
let mut app = AppState::new("test".to_string());
crate::config::parse_config_line(&mut app, "set-hook client-attached 'cmd-a'");
crate::config::parse_config_line(&mut app, "set-hook -a client-attached 'cmd-b'");
let cmds = app.hooks.get("client-attached").unwrap();
assert_eq!(cmds.len(), 2, "-a without -g should also append");
}
#[test]
fn warm_server_is_disabled_for_destroy_unattached_sessions() {
let mut app = AppState::new("demo".to_string());
app.destroy_unattached = true;
assert!(!should_spawn_warm_server(&app));
}
#[test]
fn warm_server_is_disabled_for_warm_session_itself() {
let app = AppState::new("__warm__".to_string());
assert!(!should_spawn_warm_server(&app));
}
#[test]
fn warm_server_is_disabled_when_warm_enabled_is_false() {
let mut app = AppState::new("demo".to_string());
app.warm_enabled = false;
assert!(!should_spawn_warm_server(&app));
}
#[test]
fn warm_server_is_allowed_for_normal_sessions() {
let app = AppState::new("demo".to_string());
assert!(should_spawn_warm_server(&app));
}
#[test]
fn get_option_allow_rename() {
let app = AppState::new("test".to_string());
let val = super::options::get_option_value(&app, "allow-rename");
assert_eq!(val, "on");
}
#[test]
fn get_option_bell_action() {
let app = AppState::new("test".to_string());
let val = super::options::get_option_value(&app, "bell-action");
assert_eq!(val, "any");
}
#[test]
fn get_option_activity_action() {
let app = AppState::new("test".to_string());
let val = super::options::get_option_value(&app, "activity-action");
assert_eq!(val, "other");
}
#[test]
fn get_option_silence_action() {
let app = AppState::new("test".to_string());
let val = super::options::get_option_value(&app, "silence-action");
assert_eq!(val, "other");
}
#[test]
fn get_option_update_environment() {
let app = AppState::new("test".to_string());
let val = super::options::get_option_value(&app, "update-environment");
assert!(val.contains("DISPLAY"));
assert!(val.contains("SSH_AUTH_SOCK"));
}
#[test]
fn set_option_allow_rename_off() {
let mut app = AppState::new("test".to_string());
super::options::apply_set_option(&mut app, "allow-rename", "off", false);
assert!(!app.allow_rename);
}
#[test]
fn set_option_activity_action() {
let mut app = AppState::new("test".to_string());
super::options::apply_set_option(&mut app, "activity-action", "any", false);
assert_eq!(app.activity_action, "any");
}
#[test]
fn set_option_silence_action() {
let mut app = AppState::new("test".to_string());
super::options::apply_set_option(&mut app, "silence-action", "none", false);
assert_eq!(app.silence_action, "none");
}
use crossterm::event::{KeyCode, KeyModifiers};
use crate::config::{normalize_key_for_binding, parse_bind_key};
use crate::types::{Action, FocusDir};
#[test]
fn bind_key_n_creates_root_binding() {
let mut app = AppState::new("test".to_string());
parse_bind_key(&mut app, "bind-key -n C-h select-pane -L");
let root = app.key_tables.get("root").expect("root table should exist");
assert_eq!(root.len(), 1, "root table should have one binding");
let bind = &root[0];
assert!(matches!(bind.action, Action::MoveFocus(FocusDir::Left)),
"C-h should be bound to select-pane -L");
}
#[test]
fn bind_key_n_all_vim_directions() {
let mut app = AppState::new("test".to_string());
parse_bind_key(&mut app, "bind-key -n C-h select-pane -L");
parse_bind_key(&mut app, "bind-key -n C-j select-pane -D");
parse_bind_key(&mut app, "bind-key -n C-k select-pane -U");
parse_bind_key(&mut app, "bind-key -n C-l select-pane -R");
let root = app.key_tables.get("root").expect("root table should exist");
assert_eq!(root.len(), 4, "root table should have four bindings");
let expected = [
('h', FocusDir::Left),
('j', FocusDir::Down),
('k', FocusDir::Up),
('l', FocusDir::Right),
];
for (ch, dir) in expected {
let key = normalize_key_for_binding((KeyCode::Char(ch), KeyModifiers::CONTROL));
let bind = root.iter().find(|b| b.key == key)
.unwrap_or_else(|| panic!("binding for C-{} should exist", ch));
assert!(matches!(&bind.action, Action::MoveFocus(d) if *d == dir),
"C-{} should be bound to {:?}", ch, dir);
}
}
#[test]
fn ctrl_h_binding_matches_windows_key_event() {
let mut app = AppState::new("test".to_string());
parse_bind_key(&mut app, "bind-key -n C-h select-pane -L");
let root = app.key_tables.get("root").unwrap();
let win_key = normalize_key_for_binding((KeyCode::Char('h'), KeyModifiers::CONTROL));
assert!(root.iter().any(|b| b.key == win_key),
"C-h binding must match Char('h')+CONTROL key event");
}
#[test]
fn backspace_and_ctrl_h_are_distinct_on_windows() {
let backspace = normalize_key_for_binding((KeyCode::Backspace, KeyModifiers::empty()));
let ctrl_h = normalize_key_for_binding((KeyCode::Char('h'), KeyModifiers::CONTROL));
assert_ne!(backspace, ctrl_h,
"Backspace and C-h must be distinct on Windows (no Unix aliasing)");
}
#[test]
fn tab_and_ctrl_i_are_distinct_on_windows() {
let tab = normalize_key_for_binding((KeyCode::Tab, KeyModifiers::empty()));
let ctrl_i = normalize_key_for_binding((KeyCode::Char('i'), KeyModifiers::CONTROL));
assert_ne!(tab, ctrl_i,
"Tab and C-i must be distinct on Windows");
}
#[test]
fn enter_and_ctrl_m_are_distinct_on_windows() {
let enter = normalize_key_for_binding((KeyCode::Enter, KeyModifiers::empty()));
let ctrl_m = normalize_key_for_binding((KeyCode::Char('m'), KeyModifiers::CONTROL));
assert_ne!(enter, ctrl_m,
"Enter and C-m must be distinct on Windows");
}
#[test]
fn normalize_only_strips_shift_from_char() {
let shifted = normalize_key_for_binding((KeyCode::Char('A'), KeyModifiers::SHIFT));
assert_eq!(shifted, (KeyCode::Char('A'), KeyModifiers::empty()));
let ctrl_l = normalize_key_for_binding((KeyCode::Char('l'), KeyModifiers::CONTROL));
assert_eq!(ctrl_l, (KeyCode::Char('l'), KeyModifiers::CONTROL));
let shift_bs = normalize_key_for_binding((KeyCode::Backspace, KeyModifiers::SHIFT));
assert_eq!(shift_bs, (KeyCode::Backspace, KeyModifiers::SHIFT));
}
#[test]
fn bind_key_select_pane_z_stays_as_command() {
let mut app = AppState::new("test".to_string());
crate::config::parse_bind_key(&mut app, "bind-key -n C-h select-pane -Z -L");
let root = app.key_tables.get("root").expect("root table should exist");
let bind = &root[0];
assert!(matches!(&bind.action, crate::types::Action::Command(cmd) if cmd == "select-pane -Z -L"));
}
#[test]
fn parse_config_line_select_pane_z_stays_as_command() {
let mut app = AppState::new("test".to_string());
crate::config::parse_config_line(&mut app, "bind-key -r h select-pane -Z -L");
let prefix = app.key_tables.get("prefix").expect("prefix table should exist");
let bind = prefix.iter().find(|b| matches!(b.key.0, KeyCode::Char('h'))).expect("h binding should exist");
assert!(matches!(&bind.action, crate::types::Action::Command(cmd) if cmd == "select-pane -Z -L"));
}
#[test]
fn serialized_bindings_preserve_select_pane_z_command() {
let mut app = AppState::new("test".to_string());
crate::config::parse_config_line(&mut app, "bind-key -r h select-pane -Z -L");
let json = crate::server::helpers::serialize_bindings_json(&app);
assert!(json.contains("select-pane -Z -L"));
}
#[test]
fn combined_data_version_changes_on_copy_pos() {
let mut app = AppState::new("test".to_string());
let v1 = combined_data_version(&app);
app.copy_pos = Some((5, 10));
let v2 = combined_data_version(&app);
assert_ne!(v1, v2, "version must change when copy_pos is set");
app.copy_pos = Some((5, 11));
let v3 = combined_data_version(&app);
assert_ne!(v2, v3, "version must change when copy cursor column changes");
app.copy_pos = Some((6, 11));
let v4 = combined_data_version(&app);
assert_ne!(v3, v4, "version must change when copy cursor row changes");
}
#[test]
fn combined_data_version_changes_on_scroll_offset() {
let mut app = AppState::new("test".to_string());
let v1 = combined_data_version(&app);
app.copy_scroll_offset = 5;
let v2 = combined_data_version(&app);
assert_ne!(v1, v2, "version must change when copy_scroll_offset changes");
app.copy_scroll_offset = 6;
let v3 = combined_data_version(&app);
assert_ne!(v2, v3, "version must change on each scroll offset increment");
}
#[test]
fn combined_data_version_changes_on_copy_anchor() {
let mut app = AppState::new("test".to_string());
let v1 = combined_data_version(&app);
app.copy_anchor = Some((3, 7));
let v2 = combined_data_version(&app);
assert_ne!(v1, v2, "version must change when copy_anchor is set");
}
#[test]
fn combined_data_version_stable_when_copy_state_unchanged() {
let mut app = AppState::new("test".to_string());
app.copy_pos = Some((2, 3));
app.copy_scroll_offset = 10;
app.copy_anchor = Some((1, 0));
let v1 = combined_data_version(&app);
let v2 = combined_data_version(&app);
assert_eq!(v1, v2, "version must be stable when nothing changes");
}
#[test]
fn bell_forward_defaults_to_false() {
let app = AppState::new("test".to_string());
assert!(!app.bell_forward, "bell_forward must default to false");
}
#[test]
fn bell_action_none_suppresses_bell_forward() {
let mut app = AppState::new("test".to_string());
crate::config::parse_config_line(&mut app, "set -g bell-action none");
assert_eq!(app.bell_action, "none");
let hooks = super::helpers::check_window_activity(&mut app);
assert!(!app.bell_forward, "bell_forward must stay false with bell-action none");
assert!(hooks.is_empty());
}
#[test]
fn bell_action_set_to_any_via_config() {
let mut app = AppState::new("test".to_string());
crate::config::parse_config_line(&mut app, "set -g bell-action any");
assert_eq!(app.bell_action, "any");
}
#[test]
fn bell_action_set_to_current_via_config() {
let mut app = AppState::new("test".to_string());
crate::config::parse_config_line(&mut app, "set -g bell-action current");
assert_eq!(app.bell_action, "current");
}
#[test]
fn bell_action_set_to_other_via_config() {
let mut app = AppState::new("test".to_string());
crate::config::parse_config_line(&mut app, "set -g bell-action other");
assert_eq!(app.bell_action, "other");
}
fn mock_window_for_server(name: &str) -> crate::types::Window {
crate::types::Window {
root: crate::types::Node::Split {
kind: crate::types::LayoutKind::Horizontal,
sizes: vec![],
children: vec![],
},
active_path: vec![],
name: name.to_string(),
id: 0,
activity_flag: false,
bell_flag: false,
silence_flag: false,
last_output_time: std::time::Instant::now(),
last_seen_version: 0,
manual_rename: false,
layout_index: 0,
pane_mru: vec![],
zoom_saved: None,
linked_from: None,
}
}
#[test]
fn list_windows_tab_text_reflects_zoom_flag() {
let mut app = AppState::new("test".to_string());
app.window_status_current_format = "#W #{?window_zoomed_flag,+, }".to_string();
app.window_status_format = "#W #{?window_zoomed_flag,+, }".to_string();
let mut win0 = mock_window_for_server("editor");
win0.id = 0;
app.windows.push(win0);
app.active_idx = 0;
let json_before = list_windows_json_with_tabs(&app).unwrap();
assert!(json_before.contains("editor ") || !json_before.contains("editor +"),
"before zoom, tab_text should not show +, got: {}", json_before);
app.windows[0].zoom_saved = Some(vec![(vec![], vec![50, 50])]);
let json_after = list_windows_json_with_tabs(&app).unwrap();
assert!(json_after.contains("editor +"),
"after zoom, tab_text must show +, got: {}", json_after);
}
#[test]
fn list_windows_tab_text_per_window_zoom() {
let mut app = AppState::new("test".to_string());
app.window_status_current_format = "#I #W #{?window_zoomed_flag,+, }".to_string();
app.window_status_format = "#I #W #{?window_zoomed_flag,+, }".to_string();
let mut win0 = mock_window_for_server("editor");
win0.id = 0;
win0.zoom_saved = Some(vec![(vec![], vec![50, 50])]);
let mut win1 = mock_window_for_server("shell");
win1.id = 1;
app.windows.push(win0);
app.windows.push(win1);
app.active_idx = 1;
let json = list_windows_json_with_tabs(&app).unwrap();
assert!(json.contains("editor +"), "zoomed window 0 must show +, got: {}", json);
assert!(!json.contains("shell +"), "non-zoomed window 1 must not show +, got: {}", json);
}