use super::*;
use crate::app::hop::{HopKind, assign_labels};
#[test]
fn assign_labels_zero() {
assert!(assign_labels(0).is_empty());
}
#[test]
fn assign_labels_single() {
let l = assign_labels(1);
assert_eq!(l, vec!["a"]);
}
#[test]
fn assign_labels_52_all_one_char() {
let labels = assign_labels(52);
assert_eq!(labels.len(), 52);
for l in &labels {
assert_eq!(l.chars().count(), 1, "expected 1-char label, got {l:?}");
}
let unique: std::collections::HashSet<_> = labels.iter().collect();
assert_eq!(unique.len(), 52);
}
#[test]
fn assign_labels_53_all_two_char() {
let labels = assign_labels(53);
assert_eq!(labels.len(), 53);
for l in &labels {
assert_eq!(l.chars().count(), 2, "expected 2-char label, got {l:?}");
}
let unique: std::collections::HashSet<_> = labels.iter().collect();
assert_eq!(unique.len(), 53);
for (i, a) in labels.iter().enumerate() {
for (j, b) in labels.iter().enumerate() {
if i != j && a.len() < b.len() {
assert!(
!b.starts_with(a.as_str()),
"label {a:?} is a prefix of {b:?}"
);
}
}
}
}
#[test]
fn assign_labels_large() {
let labels = assign_labels(200);
assert_eq!(labels.len(), 200);
let unique: std::collections::HashSet<_> = labels.iter().collect();
assert_eq!(unique.len(), 200);
}
#[test]
fn hop_word_populates_targets() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello world foo\nbar baz");
app.start_hop(HopKind::Word);
let hop = app.hop.as_ref().expect("hop should be active");
assert!(!hop.targets.is_empty(), "expected word targets");
for t in &hop.targets {
assert!(!t.label.is_empty());
}
}
#[test]
fn hop_esc_cancels_and_clears() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello world");
app.start_hop(HopKind::Word);
assert!(app.hop.is_some());
let (orig_row, orig_col) = app.active_editor().cursor();
app.hop_handle_key(None, true);
assert!(app.hop.is_none(), "hop should be cleared after Esc");
let (row, col) = app.active_editor().cursor();
assert_eq!((row, col), (orig_row, orig_col));
}
#[test]
fn hop_label_resolves_cursor() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello world");
app.active_editor_mut().jump_cursor(0, 0);
app.sync_after_engine_mutation();
app.start_hop(HopKind::Word);
let hop = app.hop.as_ref().unwrap();
let (target_row, target_col, label) = hop
.targets
.iter()
.find(|t| t.col == 6)
.map(|t| (t.row, t.col, t.label.clone()))
.expect("expected target at col 6 for 'world'");
for c in label.chars() {
app.hop_handle_key(Some(c), false);
}
assert!(app.hop.is_none(), "hop should resolve after typing label");
let (row, col) = app.active_editor().cursor();
assert_eq!(row, target_row);
assert_eq!(col, target_col);
}
#[test]
fn hop_non_matching_label_cancels() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello world");
app.start_hop(HopKind::Word);
assert!(app.hop.is_some());
app.hop_handle_key(Some('1'), false);
assert!(app.hop.is_none(), "hop should cancel on no-match key");
}
#[test]
fn hop_word_cap_targets() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello world-foo bar");
app.start_hop(HopKind::WordCap);
let hop = app.hop.as_ref().expect("hop should be active");
let row0: Vec<_> = hop.targets.iter().filter(|t| t.row == 0).collect();
assert_eq!(
row0.len(),
3,
"expected 3 WORD starts on 'hello world-foo bar'"
);
assert_eq!(row0[0].col, 0);
assert_eq!(row0[1].col, 6);
assert_eq!(row0[2].col, 16);
}
#[test]
fn hop_line_below_targets() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "line0\n line1\nline2");
app.active_editor_mut().jump_cursor(0, 0);
app.sync_after_engine_mutation();
app.start_hop(HopKind::LineBelow);
let hop = app.hop.as_ref().expect("hop should be active");
let rows: Vec<usize> = hop.targets.iter().map(|t| t.row).collect();
assert!(rows.contains(&1), "expected row 1 target");
assert!(rows.contains(&2), "expected row 2 target");
assert!(
!rows.contains(&0),
"row 0 (cursor row) should not be included"
);
let line1_target = hop.targets.iter().find(|t| t.row == 1).unwrap();
assert_eq!(
line1_target.col, 2,
"first-non-blank of ' line1' should be col 2"
);
}
#[test]
fn hop_line_above_targets() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "line0\nline1\nline2");
app.active_editor_mut().jump_cursor(2, 0);
app.sync_after_engine_mutation();
app.start_hop(HopKind::LineAbove);
let hop = app.hop.as_ref().expect("hop should be active");
let rows: Vec<usize> = hop.targets.iter().map(|t| t.row).collect();
assert!(rows.contains(&0), "expected row 0 target");
assert!(rows.contains(&1), "expected row 1 target");
assert!(!rows.contains(&2), "cursor row should not be included");
}
#[test]
fn hop_no_targets_does_not_activate() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "");
app.start_hop(HopKind::Word);
assert!(app.hop.is_none(), "hop should not activate on empty buffer");
}