use super::*;
#[test]
fn colon_write_blocked_by_disk_state_guard_without_bang() {
let path = std::env::temp_dir().join("hjkl_write_no_bang_guard.txt");
std::fs::write(&path, "original\n").unwrap();
let mut app = App::new(Some(path.clone()), false, None, None).unwrap();
app.active_mut().disk_state = DiskState::ChangedOnDisk;
app.active_mut().dirty = true;
seed_buffer(&mut app, "edited\n");
app.dispatch_ex("write");
let msg = app.bus.last_body_or_empty().to_string();
assert!(
msg.contains("E13"),
"expected E13 guard message, got: {msg}"
);
let on_disk = std::fs::read_to_string(&path).unwrap();
assert_eq!(
on_disk, "original\n",
"disk must be unchanged after blocked :w"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn colon_write_bang_overrides_disk_state_guard() {
let path = std::env::temp_dir().join("hjkl_write_bang_guard.txt");
std::fs::write(&path, "original\n").unwrap();
let mut app = App::new(Some(path.clone()), false, None, None).unwrap();
app.active_mut().disk_state = DiskState::ChangedOnDisk;
app.active_mut().dirty = true;
seed_buffer(&mut app, "edited\n");
app.dispatch_ex("write!");
let msg = app.bus.last_body_or_empty().to_string();
assert!(!msg.contains("E13"), ":w! must not produce E13, got: {msg}");
assert_eq!(
app.active().disk_state,
DiskState::Synced,
"disk_state must be Synced after :w!"
);
let on_disk = std::fs::read_to_string(&path).unwrap();
assert!(
on_disk.contains("edited"),
"disk must have new content after :w!"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn edit_percent_reloads_current_file() {
let path = std::env::temp_dir().join("hjkl_edit_percent_reload.txt");
std::fs::write(&path, "first\nsecond\n").unwrap();
let mut app = App::new(Some(path.clone()), false, None, None).unwrap();
std::fs::write(&path, "alpha\nbeta\ngamma\n").unwrap();
app.dispatch_ex("e %");
let lines = app.active().editor.buffer().lines();
assert_eq!(lines, vec!["alpha", "beta", "gamma"]);
let _ = std::fs::remove_file(&path);
}
#[test]
fn edit_no_arg_reloads_current_file() {
let path = std::env::temp_dir().join("hjkl_edit_noarg_reload.txt");
std::fs::write(&path, "v1\n").unwrap();
let mut app = App::new(Some(path.clone()), false, None, None).unwrap();
std::fs::write(&path, "v2\n").unwrap();
app.dispatch_ex("e");
assert_eq!(app.active().editor.buffer().lines(), vec!["v2".to_string()]);
let _ = std::fs::remove_file(&path);
}
#[test]
fn edit_blocks_dirty_buffer_without_force() {
let path = std::env::temp_dir().join("hjkl_edit_dirty_block.txt");
std::fs::write(&path, "orig\n").unwrap();
let mut app = App::new(Some(path.clone()), false, None, None).unwrap();
app.active_mut().dirty = true;
app.dispatch_ex("e %");
let msg = app.bus.last_body_or_empty().to_string();
assert!(msg.contains("E37"), "expected E37, got: {msg}");
let _ = std::fs::remove_file(&path);
}
#[test]
fn edit_force_reloads_dirty_buffer() {
let path = std::env::temp_dir().join("hjkl_edit_force.txt");
std::fs::write(&path, "disk\n").unwrap();
let mut app = App::new(Some(path.clone()), false, None, None).unwrap();
app.active_mut().dirty = true;
app.dispatch_ex("e!");
assert_eq!(
app.active().editor.buffer().lines(),
vec!["disk".to_string()]
);
assert!(!app.active().dirty);
let _ = std::fs::remove_file(&path);
}
#[test]
fn undo_to_saved_state_clears_dirty() {
let path = std::env::temp_dir().join("hjkl_undo_clears_dirty.txt");
std::fs::write(&path, "alpha\nbravo\n").unwrap();
let mut app = App::new(Some(path.clone()), false, None, None).unwrap();
assert!(!app.active().dirty);
hjkl_vim::handle_key(&mut app.active_mut().editor, key(KeyCode::Char('i')));
hjkl_vim::handle_key(&mut app.active_mut().editor, key(KeyCode::Char('X')));
if app.active_mut().editor.take_dirty() {
app.active_mut().refresh_dirty_against_saved();
}
assert!(app.active().dirty, "edit should mark dirty");
hjkl_vim::handle_key(&mut app.active_mut().editor, key(KeyCode::Esc));
hjkl_vim::handle_key(&mut app.active_mut().editor, key(KeyCode::Char('u')));
if app.active_mut().editor.take_dirty() {
app.active_mut().refresh_dirty_against_saved();
}
assert!(
!app.active().dirty,
"undo to saved state should clear dirty"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn edit_new_path_appends_slot_and_switches() {
let path_a = std::env::temp_dir().join("hjkl_phc_a.txt");
let path_b = std::env::temp_dir().join("hjkl_phc_b.txt");
std::fs::write(&path_a, "alpha\n").unwrap();
std::fs::write(&path_b, "beta\n").unwrap();
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
assert_eq!(app.slots.len(), 1);
app.dispatch_ex(&format!("e {}", path_b.display()));
assert_eq!(app.slots.len(), 2);
assert_eq!(app.active_index(), 1);
assert_eq!(
app.active().editor.buffer().lines(),
vec!["beta".to_string()]
);
let _ = std::fs::remove_file(&path_a);
let _ = std::fs::remove_file(&path_b);
}
#[test]
fn edit_existing_path_switches_to_open_slot() {
let path_a = std::env::temp_dir().join("hjkl_phc_switch_a.txt");
let path_b = std::env::temp_dir().join("hjkl_phc_switch_b.txt");
std::fs::write(&path_a, "alpha\n").unwrap();
std::fs::write(&path_b, "beta\n").unwrap();
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path_b.display()));
assert_eq!(app.active_index(), 1);
app.dispatch_ex(&format!("e {}", path_a.display()));
assert_eq!(app.slots.len(), 2);
assert_eq!(app.active_index(), 0);
let _ = std::fs::remove_file(&path_a);
let _ = std::fs::remove_file(&path_b);
}
#[test]
fn edit_other_open_path_does_not_block_on_dirty() {
let path_a = std::env::temp_dir().join("hjkl_phc_dirty_a.txt");
let path_b = std::env::temp_dir().join("hjkl_phc_dirty_b.txt");
std::fs::write(&path_a, "a\n").unwrap();
std::fs::write(&path_b, "b\n").unwrap();
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.active_mut().dirty = true;
app.dispatch_ex(&format!("e {}", path_b.display()));
assert_eq!(app.active_index(), 1);
assert!(app.slots[0].dirty, "slot 0 should remain dirty");
let _ = std::fs::remove_file(&path_a);
let _ = std::fs::remove_file(&path_b);
}
#[test]
fn bnext_bprev_cycle_active() {
let path_a = std::env::temp_dir().join("hjkl_phc_cycle_a.txt");
let path_b = std::env::temp_dir().join("hjkl_phc_cycle_b.txt");
let path_c = std::env::temp_dir().join("hjkl_phc_cycle_c.txt");
for p in [&path_a, &path_b, &path_c] {
std::fs::write(p, "x\n").unwrap();
}
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path_b.display()));
app.dispatch_ex(&format!("e {}", path_c.display()));
assert_eq!(app.active_index(), 2);
app.dispatch_ex("bn");
assert_eq!(app.active_index(), 0, "wrap forward to 0");
app.dispatch_ex("bn");
assert_eq!(app.active_index(), 1);
app.dispatch_ex("bp");
assert_eq!(app.active_index(), 0);
app.dispatch_ex("bp");
assert_eq!(app.active_index(), 2, "wrap backward to last");
for p in [&path_a, &path_b, &path_c] {
let _ = std::fs::remove_file(p);
}
}
#[test]
fn bnext_no_op_with_single_slot() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("bn");
assert_eq!(app.active_index(), 0);
assert_eq!(app.slots.len(), 1);
}
#[test]
fn bdelete_blocks_dirty_without_force() {
let path_a = std::env::temp_dir().join("hjkl_phc_bd_a.txt");
let path_b = std::env::temp_dir().join("hjkl_phc_bd_b.txt");
std::fs::write(&path_a, "a\n").unwrap();
std::fs::write(&path_b, "b\n").unwrap();
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path_b.display()));
app.active_mut().dirty = true;
app.dispatch_ex("bd");
let msg = app.bus.last_body_or_empty().to_string();
assert!(msg.contains("E89"), "expected E89, got: {msg}");
assert_eq!(app.slots.len(), 2);
let _ = std::fs::remove_file(&path_a);
let _ = std::fs::remove_file(&path_b);
}
#[test]
fn bdelete_force_removes_dirty_slot() {
let path_a = std::env::temp_dir().join("hjkl_phc_bdforce_a.txt");
let path_b = std::env::temp_dir().join("hjkl_phc_bdforce_b.txt");
std::fs::write(&path_a, "a\n").unwrap();
std::fs::write(&path_b, "b\n").unwrap();
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path_b.display()));
app.active_mut().dirty = true;
app.dispatch_ex("bd!");
assert_eq!(app.slots.len(), 1);
assert_eq!(app.active_index(), 0);
assert_eq!(app.active().editor.buffer().lines(), vec!["a".to_string()]);
let _ = std::fs::remove_file(&path_a);
let _ = std::fs::remove_file(&path_b);
}
#[test]
fn bdelete_on_last_slot_resets_to_no_name() {
let path = std::env::temp_dir().join("hjkl_phc_bd_last.txt");
std::fs::write(&path, "content\n").unwrap();
let mut app = App::new(Some(path.clone()), false, None, None).unwrap();
app.dispatch_ex("bd");
assert_eq!(app.slots.len(), 1);
assert!(app.active().filename.is_none());
let lines = app.active().editor.buffer().lines();
assert!(
lines.is_empty() || (lines.len() == 1 && lines[0].is_empty()),
"expected empty scratch buffer, got: {lines:?}"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn bwipeout_clears_marks_on_last_slot() {
let path = std::env::temp_dir().join("hjkl_bwipeout_marks_last.txt");
std::fs::write(&path, "hello\n").unwrap();
let mut app = App::new(Some(path.clone()), false, None, None).unwrap();
app.active_mut().editor.set_mark('a', (0, 0));
assert!(
app.active().editor.mark('a').is_some(),
"mark should be set before wipe"
);
app.dispatch_ex("bw");
assert!(
app.active().editor.mark('a').is_none(),
":bwipeout on last slot must not carry marks into scratch buffer"
);
assert_eq!(app.slots.len(), 1);
assert!(app.active().filename.is_none());
let _ = std::fs::remove_file(&path);
}
#[test]
fn bwipeout_clears_jumplist_on_last_slot() {
let path = std::env::temp_dir().join("hjkl_bwipeout_jumps_last.txt");
std::fs::write(&path, "line1\nline2\nline3\n").unwrap();
let mut app = App::new(Some(path.clone()), false, None, None).unwrap();
app.active_mut().editor.record_jump((2, 0));
let (back, _) = app.active().editor.jump_list();
assert!(
!back.is_empty(),
"jumplist should have an entry before wipe"
);
app.dispatch_ex("bw");
let (back, fwd) = app.active().editor.jump_list();
assert!(
back.is_empty() && fwd.is_empty(),
":bwipeout on last slot must not carry jumps into scratch buffer"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn bdelete_does_not_explicitly_wipe_marks_path() {
let path_a = std::env::temp_dir().join("hjkl_bdelete_path_a.txt");
let path_b = std::env::temp_dir().join("hjkl_bdelete_path_b.txt");
std::fs::write(&path_a, "a\n").unwrap();
std::fs::write(&path_b, "b\n").unwrap();
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path_b.display()));
assert_eq!(app.slots.len(), 2);
app.active_mut().editor.set_mark('z', (0, 0));
app.dispatch_ex("bd");
assert_eq!(app.slots.len(), 1);
assert!(
app.active().editor.mark('z').is_none(),
"mark from removed slot must not survive after :bd"
);
let _ = std::fs::remove_file(&path_a);
let _ = std::fs::remove_file(&path_b);
}
#[test]
fn bwipeout_multi_slot_removes_slot() {
let path_a = std::env::temp_dir().join("hjkl_bwipeout_multi_a.txt");
let path_b = std::env::temp_dir().join("hjkl_bwipeout_multi_b.txt");
std::fs::write(&path_a, "a\n").unwrap();
std::fs::write(&path_b, "b\n").unwrap();
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path_b.display()));
assert_eq!(app.slots.len(), 2);
app.dispatch_ex("bw");
assert_eq!(app.slots.len(), 1);
let msg = app.bus.last_body_or_empty().to_string();
assert!(
msg.contains("wiped"),
"expected wipe status message, got: {msg}"
);
let _ = std::fs::remove_file(&path_a);
let _ = std::fs::remove_file(&path_b);
}
#[test]
fn bwipeout_blocks_dirty_without_force() {
let path_a = std::env::temp_dir().join("hjkl_bwipeout_dirty_a.txt");
let path_b = std::env::temp_dir().join("hjkl_bwipeout_dirty_b.txt");
std::fs::write(&path_a, "a\n").unwrap();
std::fs::write(&path_b, "b\n").unwrap();
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path_b.display()));
app.active_mut().dirty = true;
app.dispatch_ex("bw");
let msg = app.bus.last_body_or_empty().to_string();
assert!(msg.contains("E89"), "expected E89, got: {msg}");
assert_eq!(
app.slots.len(),
2,
"slot must not be removed when dirty without force"
);
let _ = std::fs::remove_file(&path_a);
let _ = std::fs::remove_file(&path_b);
}
#[test]
fn buffer_alt_swaps_with_prev_active() {
let path_a = std::env::temp_dir().join("hjkl_d2_alt_a.txt");
let path_b = std::env::temp_dir().join("hjkl_d2_alt_b.txt");
let path_c = std::env::temp_dir().join("hjkl_d2_alt_c.txt");
for p in [&path_a, &path_b, &path_c] {
std::fs::write(p, "x\n").unwrap();
}
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path_b.display())); app.dispatch_ex(&format!("e {}", path_c.display())); assert_eq!(app.active_index(), 2);
assert_eq!(app.prev_active, Some(1));
app.buffer_alt();
assert_eq!(app.active_index(), 1);
assert_eq!(app.prev_active, Some(2));
app.buffer_alt();
assert_eq!(app.active_index(), 2);
for p in [&path_a, &path_b, &path_c] {
let _ = std::fs::remove_file(p);
}
}
#[test]
fn buffer_alt_with_single_slot_no_op_with_message() {
let mut app = App::new(None, false, None, None).unwrap();
assert_eq!(app.slots.len(), 1);
app.buffer_alt();
assert_eq!(app.active_index(), 0);
let msg = app.bus.last_body_or_empty().to_string();
assert!(
msg.contains("only one buffer"),
"expected 'only one buffer' message, got: {msg}"
);
}
#[test]
fn bd_clears_prev_active() {
let path_a = std::env::temp_dir().join("hjkl_d2_bd_a.txt");
let path_b = std::env::temp_dir().join("hjkl_d2_bd_b.txt");
std::fs::write(&path_a, "a\n").unwrap();
std::fs::write(&path_b, "b\n").unwrap();
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path_b.display())); assert_eq!(app.prev_active, Some(0));
app.dispatch_ex("bd!");
assert!(
app.prev_active.is_none(),
"prev_active should be None after bd!"
);
let _ = std::fs::remove_file(&path_a);
let _ = std::fs::remove_file(&path_b);
}
#[test]
fn b_num_switches_by_index() {
let path_a = std::env::temp_dir().join("hjkl_phe_bnum_a.txt");
let path_b = std::env::temp_dir().join("hjkl_phe_bnum_b.txt");
let path_c = std::env::temp_dir().join("hjkl_phe_bnum_c.txt");
for p in [&path_a, &path_b, &path_c] {
std::fs::write(p, "x\n").unwrap();
}
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path_b.display()));
app.dispatch_ex(&format!("e {}", path_c.display()));
assert_eq!(app.slots.len(), 3);
app.dispatch_ex("b 2");
assert_eq!(app.active_index(), 1, "`:b 2` should switch to index 1");
for p in [&path_a, &path_b, &path_c] {
let _ = std::fs::remove_file(p);
}
}
#[test]
fn b_num_out_of_range_errors() {
let mut app = App::new(None, false, None, None).unwrap();
assert_eq!(app.slots.len(), 1);
app.dispatch_ex("b 5");
let msg = app.bus.last_body_or_empty().to_string();
assert!(msg.contains("E86"), "expected E86, got: {msg}");
}
#[test]
fn b_name_substring_switches() {
let path_foo = std::env::temp_dir().join("hjkl_phe_bname_foo.txt");
let path_bar = std::env::temp_dir().join("hjkl_phe_bname_bar.txt");
std::fs::write(&path_foo, "foo\n").unwrap();
std::fs::write(&path_bar, "bar\n").unwrap();
let mut app = App::new(Some(path_foo.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path_bar.display()));
assert_eq!(app.active_index(), 1);
app.dispatch_ex("b foo");
assert_eq!(
app.active_index(),
0,
"`:b foo` should switch to foo's slot"
);
let _ = std::fs::remove_file(&path_foo);
let _ = std::fs::remove_file(&path_bar);
}
#[test]
fn b_name_ambiguous_errors() {
let path_a = std::env::temp_dir().join("hjkl_phe_bamb_foo_a.txt");
let path_b = std::env::temp_dir().join("hjkl_phe_bamb_foo_b.txt");
std::fs::write(&path_a, "a\n").unwrap();
std::fs::write(&path_b, "b\n").unwrap();
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path_b.display()));
app.dispatch_ex("b foo");
let msg = app.bus.last_body_or_empty().to_string();
assert!(
msg.contains("E93"),
"expected E93 ambiguous error, got: {msg}"
);
let _ = std::fs::remove_file(&path_a);
let _ = std::fs::remove_file(&path_b);
}
#[test]
fn bfirst_blast_jump_to_ends() {
let path_a = std::env::temp_dir().join("hjkl_phe_bfl_a.txt");
let path_b = std::env::temp_dir().join("hjkl_phe_bfl_b.txt");
let path_c = std::env::temp_dir().join("hjkl_phe_bfl_c.txt");
for p in [&path_a, &path_b, &path_c] {
std::fs::write(p, "x\n").unwrap();
}
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path_b.display()));
app.dispatch_ex(&format!("e {}", path_c.display()));
assert_eq!(app.slots.len(), 3);
app.dispatch_ex("b 2");
assert_eq!(app.active_index(), 1);
app.dispatch_ex("bfirst");
assert_eq!(app.active_index(), 0, "`:bfirst` should go to slot 0");
app.dispatch_ex("blast");
assert_eq!(app.active_index(), 2, "`:blast` should go to last slot");
for p in [&path_a, &path_b, &path_c] {
let _ = std::fs::remove_file(p);
}
}
#[test]
fn wa_writes_dirty_named_slots() {
let path_a = std::env::temp_dir().join("hjkl_phe_wa_a.txt");
let path_b = std::env::temp_dir().join("hjkl_phe_wa_b.txt");
std::fs::write(&path_a, "original a\n").unwrap();
std::fs::write(&path_b, "original b\n").unwrap();
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path_b.display()));
app.slots[0].dirty = true;
BufferEdit::replace_all(app.slots[0].editor.buffer_mut(), "edited a");
app.slots[1].dirty = true;
BufferEdit::replace_all(app.slots[1].editor.buffer_mut(), "edited b");
app.dispatch_ex("wa");
assert!(!app.slots[0].dirty, "slot 0 should be clean after :wa");
assert!(!app.slots[1].dirty, "slot 1 should be clean after :wa");
let contents_a = std::fs::read_to_string(&path_a).unwrap_or_default();
let contents_b = std::fs::read_to_string(&path_b).unwrap_or_default();
assert!(
contents_a.contains("edited a"),
"file a should contain edited content, got: {contents_a}"
);
assert!(
contents_b.contains("edited b"),
"file b should contain edited content, got: {contents_b}"
);
let _ = std::fs::remove_file(&path_a);
let _ = std::fs::remove_file(&path_b);
}
#[test]
fn qa_blocks_when_any_slot_dirty() {
let path_a = std::env::temp_dir().join("hjkl_phe_qa_dirty_a.txt");
let path_b = std::env::temp_dir().join("hjkl_phe_qa_dirty_b.txt");
std::fs::write(&path_a, "a\n").unwrap();
std::fs::write(&path_b, "b\n").unwrap();
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path_b.display()));
app.slots[0].dirty = true;
app.dispatch_ex("qa");
assert!(
!app.exit_requested,
":qa should not exit when dirty slot exists"
);
let msg = app.bus.last_body_or_empty().to_string();
assert!(msg.contains("E37"), "expected E37, got: {msg}");
let _ = std::fs::remove_file(&path_a);
let _ = std::fs::remove_file(&path_b);
}
#[test]
fn qa_force_exits_with_dirty() {
let path_a = std::env::temp_dir().join("hjkl_phe_qa_force_a.txt");
std::fs::write(&path_a, "a\n").unwrap();
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.slots[0].dirty = true;
app.dispatch_ex("qa!");
assert!(app.exit_requested, ":qa! should exit even when dirty");
let _ = std::fs::remove_file(&path_a);
}
#[test]
fn q_on_multi_slot_closes_slot_not_app() {
let path_a = std::env::temp_dir().join("hjkl_phe_q_multi_a.txt");
let path_b = std::env::temp_dir().join("hjkl_phe_q_multi_b.txt");
std::fs::write(&path_a, "a\n").unwrap();
std::fs::write(&path_b, "b\n").unwrap();
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path_b.display()));
assert_eq!(app.slots.len(), 2);
app.dispatch_ex("q!");
assert_eq!(
app.slots.len(),
1,
"`:q!` with 2 slots should close active slot"
);
assert!(
!app.exit_requested,
"app should remain open after closing one slot"
);
let _ = std::fs::remove_file(&path_a);
let _ = std::fs::remove_file(&path_b);
}
#[test]
fn q_on_last_slot_quits_app() {
let mut app = App::new(None, false, None, None).unwrap();
assert_eq!(app.slots.len(), 1);
assert!(!app.active().dirty);
app.dispatch_ex("q");
assert!(app.exit_requested, "`:q` on clean last slot should exit");
}
#[test]
fn q_bang_force_quits_dirty_buffer_via_hjkl_ex() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "unsaved work");
app.active_mut().dirty = true;
app.dispatch_ex("q!");
assert!(
app.exit_requested,
"`:q!` must force-quit a dirty buffer (hjkl-ex Phase 1 routing)"
);
}
#[test]
fn checktime_reloads_clean_buffer_when_disk_changed() {
let path = std::env::temp_dir().join("hjkl_ct_reload.txt");
std::fs::write(&path, "line1\nline2\n").unwrap();
let mut app = App::new(Some(path.clone()), false, None, None).unwrap();
assert_eq!(app.active().editor.buffer().lines(), vec!["line1", "line2"]);
write_and_wait(&path, "new content\n");
app.checktime_all();
assert_eq!(
app.active().editor.buffer().lines(),
vec!["new content"],
"buffer should be reloaded from disk"
);
assert!(!app.active().dirty, "reloaded buffer must not be dirty");
assert_eq!(app.active().disk_state, DiskState::Synced);
let _ = std::fs::remove_file(&path);
}
#[test]
fn checktime_marks_dirty_buffer_as_changed_on_disk_no_reload() {
let path = std::env::temp_dir().join("hjkl_ct_dirty.txt");
std::fs::write(&path, "original\n").unwrap();
let mut app = App::new(Some(path.clone()), false, None, None).unwrap();
app.active_mut().dirty = true;
write_and_wait(&path, "changed on disk\n");
app.checktime_all();
assert_eq!(
app.active().editor.buffer().lines(),
vec!["original"],
"dirty buffer must not be reloaded"
);
assert_eq!(
app.active().disk_state,
DiskState::ChangedOnDisk,
"disk_state must be ChangedOnDisk"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn checktime_marks_deleted_when_file_removed() {
let path = std::env::temp_dir().join("hjkl_ct_deleted.txt");
std::fs::write(&path, "content\n").unwrap();
let mut app = App::new(Some(path.clone()), false, None, None).unwrap();
std::fs::remove_file(&path).unwrap();
app.checktime_all();
assert_eq!(app.active().disk_state, DiskState::DeletedOnDisk);
assert_eq!(app.active().editor.buffer().lines(), vec!["content"]);
}
#[test]
fn checktime_recovers_after_file_recreated() {
let path = std::env::temp_dir().join("hjkl_ct_recover.txt");
std::fs::write(&path, "v1\n").unwrap();
let mut app = App::new(Some(path.clone()), false, None, None).unwrap();
std::fs::remove_file(&path).unwrap();
app.checktime_all();
assert_eq!(app.active().disk_state, DiskState::DeletedOnDisk);
write_and_wait(&path, "v2\n");
app.checktime_all();
assert_eq!(
app.active().editor.buffer().lines(),
vec!["v2"],
"recreated file should be reloaded"
);
assert_eq!(app.active().disk_state, DiskState::Synced);
let _ = std::fs::remove_file(&path);
}
#[test]
fn substitute_percent_global_multi_line() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "foo foo\nfoo");
app.dispatch_ex("%s/foo/bar/g");
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines,
vec!["bar bar", "bar"],
"buffer should be fully substituted"
);
let msg = app.bus.last_body_or_empty().to_string();
assert_eq!(msg, "3 substitutions on 2 lines", "status: {msg}");
}
#[test]
fn substitute_current_line_first_only() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "foo foo\nfoo");
app.dispatch_ex("s/foo/bar/");
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(lines[0], "bar foo", "only first occurrence on current line");
assert_eq!(lines[1], "foo", "second line unchanged");
let msg = app.bus.last_body_or_empty().to_string();
assert_eq!(msg, "1 substitutions on 1 lines", "status: {msg}");
}
#[test]
fn substitute_empty_pattern_reuses_last_search() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello world");
app.active_mut()
.editor
.set_last_search(Some("world".to_string()), true);
app.dispatch_ex("s//planet/");
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines[0], "hello planet",
"should replace using last search pattern"
);
let msg = app.bus.last_body_or_empty().to_string();
assert_eq!(msg, "1 substitutions on 1 lines", "status: {msg}");
}
#[test]
fn substitute_no_match_shows_pattern_not_found() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello world");
app.dispatch_ex("s/xyz/bar/");
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(lines[0], "hello world", "buffer should be unchanged");
let msg = app.bus.last_body_or_empty().to_string();
assert_eq!(msg, "Pattern not found", "status: {msg}");
}
#[test]
fn anvil_install_unknown_tool_sets_error_message() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("Anvil install definitely-not-a-real-tool-xyz");
let msg = app.bus.last_body_or_empty().to_string();
assert!(
msg.contains("unknown tool"),
"expected 'unknown tool' in status message, got: {msg:?}"
);
}
#[test]
fn anvil_uninstall_not_installed_graceful() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("Anvil uninstall rust-analyzer");
let msg = app.bus.last_body_or_empty().to_string();
assert!(
!msg.is_empty(),
"expected some status message after anvil uninstall"
);
}
#[test]
fn anvil_update_all_with_zero_installed_tools() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("Anvil update");
let msg = app.bus.last_body_or_empty().to_string();
assert!(
msg.contains("update sweep started"),
"expected 'update sweep started', got: {msg:?}"
);
}
#[test]
fn anvil_picker_source_builds_from_registry() {
use crate::picker_sources::{AnvilPickerSource, AnvilState};
let registry = hjkl_anvil::Registry::embedded().expect("embedded registry must load");
let source = AnvilPickerSource::from_registry(®istry);
assert!(!source.items.is_empty(), "picker source must have items");
for item in &source.items {
let label = item.label();
assert!(
label.contains(&item.name),
"label must contain tool name; got: {label:?}"
);
assert!(
matches!(
item.state,
AnvilState::Available | AnvilState::Installed { .. } | AnvilState::Outdated { .. }
),
"state must be one of the three variants"
);
}
}
#[test]
fn anvil_bad_subcommand_shows_usage() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("Anvil badsubcommand something else");
let msg = app.bus.last_body_or_empty().to_string();
assert!(
msg.contains("usage"),
"expected usage hint in status message, got: {msg:?}"
);
}
#[test]
fn unbound_chord_tail_trie_returns_multi_key_replay() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "abcdef");
let leader = app.config.editor.leader;
let mut replay: Vec<hjkl_keymap::KeyEvent> = Vec::new();
let consumed1 = app.dispatch_keymap(
hjkl_keymap::KeyEvent::new(
hjkl_keymap::KeyCode::Char(leader),
hjkl_keymap::KeyModifiers::NONE,
),
1,
&mut replay,
);
assert!(consumed1, "leader should be consumed as Pending prefix");
replay.clear();
let consumed2 = app.dispatch_keymap(
hjkl_keymap::KeyEvent::new(
hjkl_keymap::KeyCode::Char('x'),
hjkl_keymap::KeyModifiers::NONE,
),
1,
&mut replay,
);
assert!(!consumed2, "<leader>x is unbound → consumed=false");
assert!(
replay.len() > 1,
"replay should contain both keys, got {} keys",
replay.len()
);
}
#[test]
fn colon_e_path_opens_file_via_hjkl_ex() {
let path = tmp_path("hjkl_ex_2b_edit_test.txt");
std::fs::write(&path, "hello from hjkl-ex\n").unwrap();
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path.display()));
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines,
vec!["hello from hjkl-ex"],
"`:e <path>` must load the file content into the active buffer; got {lines:?}"
);
let active_path = app
.active()
.filename
.as_deref()
.unwrap_or(std::path::Path::new(""));
assert_eq!(
active_path,
path.as_path(),
"`:e <path>` must set the active slot filename to the opened path"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn colon_bd_via_hjkl_ex_clears_sole_buffer() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "some content");
app.active_mut().dirty = false;
app.dispatch_ex("bd");
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines,
vec![""],
"`:bd` on sole buffer must leave an empty scratch; got {lines:?}"
);
assert!(
app.active().filename.is_none(),
"`:bd` on sole buffer must clear the filename"
);
}
#[test]
fn colon_e_percent_expands_to_current_file() {
let path = tmp_path("hjkl_phase7_percent_test.txt");
std::fs::write(&path, "phase7 percent\n").unwrap();
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path.display()));
let active_after_first_open = app
.active()
.filename
.as_deref()
.unwrap_or(std::path::Path::new(""))
.to_path_buf();
app.dispatch_ex("e %");
let active_after_percent = app
.active()
.filename
.as_deref()
.unwrap_or(std::path::Path::new(""))
.to_path_buf();
assert_eq!(
active_after_percent, active_after_first_open,
"`:e %%` must expand to the current file path; got {active_after_percent:?}"
);
assert!(
app.bus.last_body_or_empty().is_empty() || !app.bus.last_body_or_empty().starts_with('E'),
"`:e %%` must not produce an error; got: {:?}",
app.bus.last_body_or_empty()
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn colon_e_hash_expands_to_alt() {
let path_a = tmp_path("hjkl_phase7_hash_a.txt");
let path_b = tmp_path("hjkl_phase7_hash_b.txt");
std::fs::write(&path_a, "file a\n").unwrap();
std::fs::write(&path_b, "file b\n").unwrap();
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path_a.display()));
app.dispatch_ex(&format!("e {}", path_b.display()));
let active_before = app
.active()
.filename
.as_deref()
.map(|p| p.to_path_buf())
.unwrap();
assert!(
active_before.ends_with("hjkl_phase7_hash_b.txt"),
"sanity: active must be B; got {active_before:?}"
);
app.dispatch_ex("e #");
let active_after = app
.active()
.filename
.as_deref()
.map(|p| p.to_path_buf())
.unwrap();
assert!(
active_after.ends_with("hjkl_phase7_hash_a.txt"),
"`:e #` must expand to alt (file A); got {active_after:?}"
);
let _ = std::fs::remove_file(&path_a);
let _ = std::fs::remove_file(&path_b);
}
#[test]
fn colon_split_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello");
let before = app.layout().leaves().len();
app.dispatch_ex("split");
assert_eq!(
app.layout().leaves().len(),
before + 1,
":split must add one leaf"
);
}
#[test]
fn colon_sp_alias_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello");
let before = app.layout().leaves().len();
app.dispatch_ex("sp");
assert_eq!(
app.layout().leaves().len(),
before + 1,
":sp alias must add one leaf"
);
}
#[test]
fn colon_vsplit_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello");
let before = app.layout().leaves().len();
app.dispatch_ex("vsplit");
assert_eq!(
app.layout().leaves().len(),
before + 1,
":vsplit must add one leaf"
);
}
#[test]
fn colon_close_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello");
app.dispatch_ex("split");
assert_eq!(app.layout().leaves().len(), 2, "setup: need 2 leaves");
app.dispatch_ex("close");
assert_eq!(
app.layout().leaves().len(),
1,
":close must collapse back to 1 leaf"
);
}
#[test]
fn colon_tabnew_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
let before = app.tabs.len();
app.dispatch_ex("tabnew");
assert_eq!(app.tabs.len(), before + 1, ":tabnew must add a tab");
}
#[test]
fn colon_tabprev_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("tabnew");
app.dispatch_ex("tabnew");
let before = app.active_tab;
app.dispatch_ex("tabprev");
assert_eq!(
app.active_tab,
before - 1,
":tabprev must decrement active_tab"
);
}
#[test]
fn colon_tabclose_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("tabnew");
assert_eq!(app.tabs.len(), 2, "setup: need 2 tabs");
app.dispatch_ex("tabclose");
assert_eq!(app.tabs.len(), 1, ":tabclose must remove a tab");
}
#[test]
fn colon_only_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "data");
app.dispatch_ex("split");
app.dispatch_ex("split");
assert!(
app.layout().leaves().len() >= 2,
"setup: need at least 2 leaves"
);
app.dispatch_ex("only");
assert_eq!(
app.layout().leaves().len(),
1,
":only must leave exactly 1 leaf"
);
}
#[test]
fn colon_bnext_via_host_registry() {
let mut app = setup_three_slot_app();
assert_eq!(app.active_index(), 2);
app.dispatch_ex("bnext");
assert_eq!(app.active_index(), 0, ":bnext must wrap to first slot");
}
#[test]
fn colon_bn_alias_via_host_registry() {
let mut app = setup_three_slot_app();
assert_eq!(app.active_index(), 2);
app.dispatch_ex("bn");
assert_eq!(app.active_index(), 0, ":bn alias must wrap to first slot");
}
#[test]
fn colon_bprevious_via_host_registry() {
let mut app = setup_three_slot_app();
app.dispatch_ex("bprevious");
assert_eq!(app.active_index(), 1, ":bprevious must retreat one slot");
}
#[test]
fn colon_bp_alias_via_host_registry() {
let mut app = setup_three_slot_app();
app.dispatch_ex("bp");
assert_eq!(app.active_index(), 1, ":bp alias must retreat one slot");
}
#[test]
fn colon_bfirst_via_host_registry() {
let mut app = setup_three_slot_app();
assert_eq!(app.active_index(), 2);
app.dispatch_ex("bfirst");
assert_eq!(app.active_index(), 0, ":bfirst must jump to slot 0");
}
#[test]
fn colon_blast_via_host_registry() {
let mut app = setup_three_slot_app();
app.dispatch_ex("bfirst");
assert_eq!(app.active_index(), 0);
app.dispatch_ex("blast");
assert_eq!(
app.active_index(),
app.slots.len() - 1,
":blast must jump to the last slot"
);
}
#[test]
fn colon_ls_via_host_registry() {
let mut app = setup_three_slot_app();
app.dispatch_ex("ls");
let msg = app.bus.last_body_or_empty().to_string();
assert!(!msg.is_empty(), ":ls must produce a status message");
}
#[test]
fn colon_buffers_via_host_registry() {
let mut app = setup_three_slot_app();
app.dispatch_ex("buffers");
let msg = app
.info_popup
.as_ref()
.map(|p| p.content.clone())
.unwrap_or_else(|| app.bus.last_body_or_empty().to_string());
assert!(!msg.is_empty(), ":buffers must produce output");
}
#[test]
fn colon_clipboard_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("clipboard");
let msg = app
.info_popup
.as_ref()
.map(|p| p.content.clone())
.unwrap_or_else(|| app.bus.last_body_or_empty().to_string());
assert!(!msg.is_empty(), ":clipboard must produce output");
}
#[test]
fn colon_perf_toggles_overlay_on() {
let mut app = App::new(None, false, None, None).unwrap();
assert!(!app.perf_overlay, "perf_overlay must start off");
app.dispatch_ex("perf");
assert!(app.perf_overlay, ":perf must enable perf_overlay");
let msg = app.bus.last_body_or_empty().to_string();
assert!(msg.contains("on"), ":perf status must say 'on'");
}
#[test]
fn colon_perf_toggles_overlay_off() {
let mut app = App::new(None, false, None, None).unwrap();
app.perf_overlay = true;
app.dispatch_ex("perf");
assert!(!app.perf_overlay, ":perf must disable perf_overlay");
let msg = app.bus.last_body_or_empty().to_string();
assert!(msg.contains("off"), ":perf status must say 'off'");
}
#[test]
fn colon_picker_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
assert!(app.picker.is_none(), "picker must start None");
app.dispatch_ex("picker");
assert!(app.picker.is_some(), ":picker must open the picker");
}
#[test]
fn colon_rg_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("rg");
assert!(app.picker.is_some(), ":rg must open the grep picker");
}
#[test]
fn colon_rg_with_pattern_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("rg fn main");
assert!(
app.picker.is_some(),
":rg <pattern> must open the grep picker"
);
}
#[test]
fn leader_slash_opens_grep_picker() {
let mut app = App::new(None, false, None, None).unwrap();
assert!(app.picker.is_none(), "picker must start None");
app.route_chord_key(key(KeyCode::Char(' ')));
app.route_chord_key(key(KeyCode::Char('/')));
assert!(
app.picker.is_some(),
"<leader>/ must open the grep picker; status={:?}",
app.bus.last_body_or_empty()
);
}
#[test]
fn leader_slash_grep_picker_populates_items() {
if std::process::Command::new("rg")
.arg("--version")
.output()
.is_err()
&& std::process::Command::new("grep")
.arg("--version")
.output()
.is_err()
{
eprintln!("skipping: no rg or grep on PATH");
return;
}
let dir = std::env::temp_dir().join(format!(
"hjkl_grep_picker_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("findme.txt");
std::fs::write(&file, "alpha\nUNIQUE_NEEDLE_42\nomega\n").unwrap();
let orig_cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let mut app = App::new(None, false, None, None).unwrap();
app.route_chord_key(key(KeyCode::Char(' ')));
app.route_chord_key(key(KeyCode::Char('/')));
assert!(app.picker.is_some(), "<leader>/ must open the picker");
for c in "UNIQUE_NEEDLE_42".chars() {
app.handle_picker_key(key(KeyCode::Char(c)));
}
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
let mut got_match = false;
while std::time::Instant::now() < deadline {
if let Some(p) = app.picker.as_mut() {
let _ = p.refresh();
p.tick(std::time::Instant::now());
let _ = p.refresh();
}
let count = app.picker.as_ref().map(|p| p.matched()).unwrap_or(0);
if count > 0 {
got_match = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
std::env::set_current_dir(&orig_cwd).unwrap();
let _ = std::fs::remove_dir_all(&dir);
assert!(
got_match,
"rg-backed grep picker must return at least one match for the seeded UNIQUE_NEEDLE_42; \
status={:?}",
app.bus.last_body_or_empty()
);
}
#[test]
fn colon_b_numeric_via_host_registry() {
let mut app = setup_three_slot_app();
app.dispatch_ex("b 2");
assert_eq!(app.active_index(), 1, ":b 2 must switch to slot index 1");
}
#[test]
fn colon_b_nonexistent_via_host_registry() {
let mut app = setup_three_slot_app();
app.dispatch_ex("b nonexistent_buffer_xyz");
let msg = app.bus.last_body_or_empty().to_string();
assert!(
msg.contains("E94") || msg.contains("No matching"),
":b nonexistent must set error status"
);
}
#[test]
fn colon_bpicker_via_host_registry() {
let mut app = setup_three_slot_app();
assert!(app.picker.is_none());
app.dispatch_ex("bpicker");
assert!(app.picker.is_some(), ":bpicker must open the buffer picker");
}
#[test]
fn colon_checktime_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("checktime");
}
#[test]
fn colon_vnew_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
let before = app.slots.len();
app.dispatch_ex("vnew");
assert!(app.slots.len() > before, ":vnew must add a new buffer slot");
}
#[test]
fn colon_new_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
let before = app.slots.len();
app.dispatch_ex("new");
assert!(app.slots.len() > before, ":new must add a new buffer slot");
}
#[test]
fn colon_tabfirst_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("tabnew");
assert!(app.active_tab > 0 || app.tabs.len() > 1);
app.dispatch_ex("tabfirst");
assert_eq!(app.active_tab, 0, ":tabfirst must jump to tab 0");
}
#[test]
fn colon_tablast_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("tabnew");
app.dispatch_ex("tabfirst");
assert_eq!(app.active_tab, 0);
app.dispatch_ex("tablast");
let last = app.tabs.len() - 1;
assert_eq!(app.active_tab, last, ":tablast must jump to the last tab");
}
#[test]
fn colon_tabonly_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("tabnew");
app.dispatch_ex("tabnew");
assert_eq!(app.tabs.len(), 3);
app.dispatch_ex("tabfirst");
app.dispatch_ex("tabnext");
assert_eq!(app.active_tab, 1);
app.dispatch_ex("tabonly");
assert_eq!(app.tabs.len(), 1, ":tabonly must leave exactly one tab");
assert_eq!(app.active_tab, 0, ":tabonly must reset active_tab to 0");
}
#[test]
fn colon_tabs_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("tabnew");
assert_eq!(app.tabs.len(), 2);
app.info_popup = None;
app.dispatch_ex("tabs");
assert!(
app.info_popup.is_some(),
":tabs must set info_popup with tab listing"
);
let popup = app.info_popup.as_ref().unwrap();
assert!(
popup.content.contains("Tab page 1"),
"popup must list Tab page 1"
);
assert!(
popup.content.contains("Tab page 2"),
"popup must list Tab page 2"
);
}
#[test]
fn colon_lnext_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("lnext");
}
#[test]
fn colon_lopen_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("lopen");
let msg = app.bus.last_body_or_empty().to_string();
assert!(
msg.contains("no diagnostics"),
":lopen with empty diag list must set status 'no diagnostics', got: {msg}"
);
}
#[test]
fn colon_resize_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("sp");
let rect = ratatui::layout::Rect {
x: 0,
y: 0,
width: 80,
height: 40,
};
let fw = app.focused_window();
inject_split_rect(app.layout_mut(), fw, rect);
let ratio_before = if let window::LayoutTree::Split { ratio, .. } = app.layout() {
*ratio
} else {
panic!("expected Split");
};
app.dispatch_ex("resize +5");
let ratio_after = if let window::LayoutTree::Split { ratio, .. } = app.layout() {
*ratio
} else {
panic!("expected Split");
};
assert!(
ratio_after > ratio_before,
":resize +5 must grow focused window ratio: before={ratio_before} after={ratio_after}"
);
}
#[test]
fn colon_vertical_resize_via_host_registry() {
let mut app = App::new(None, false, None, None).unwrap();
app.dispatch_ex("vsp");
let rect = ratatui::layout::Rect {
x: 0,
y: 0,
width: 80,
height: 24,
};
let fw = app.focused_window();
inject_split_rect(app.layout_mut(), fw, rect);
let ratio_before = if let window::LayoutTree::Split { ratio, .. } = app.layout() {
*ratio
} else {
panic!("expected Split");
};
app.dispatch_ex("vertical resize +5");
let ratio_after = if let window::LayoutTree::Split { ratio, .. } = app.layout() {
*ratio
} else {
panic!("expected Split");
};
assert!(
ratio_after > ratio_before,
":vertical resize +5 must grow focused window width ratio: before={ratio_before} after={ratio_after}"
);
}