#[allow(clippy::wildcard_imports)]
use super::super::*;
#[allow(clippy::wildcard_imports)]
use super::helpers::*;
#[test]
fn help_overlay_scrolls_by_row_page_and_wheel() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let (_tmp, mut app) = make_app();
app.open_help(Mode::Board);
let mut terminal = Terminal::new(TestBackend::new(80, 12)).unwrap();
terminal.draw(|f| render(&app, f, f.area())).unwrap();
let metrics = app.help_metrics.get();
assert!(
metrics.max_scroll > 0,
"the test frame is meant to hide rows: {metrics:?}"
);
app.handle_key(press(KeyCode::Char('j'))).unwrap();
assert_eq!(app.help_scroll, 1, "one row down");
app.handle_key(press(KeyCode::Char('k'))).unwrap();
assert_eq!(app.help_scroll, 0, "and back");
app.handle_key(press(KeyCode::PageDown)).unwrap();
assert_eq!(app.help_scroll, app.help_page(), "one page down");
app.handle_key(press(KeyCode::PageUp)).unwrap();
assert_eq!(app.help_scroll, 0, "and back");
app.handle_mouse(scroll_down(1, 1)).unwrap();
assert_eq!(app.help_scroll, WHEEL_ROWS, "one wheel notch");
app.handle_mouse(scroll_up(1, 1)).unwrap();
assert_eq!(app.help_scroll, 0);
for _ in 0..50 {
app.handle_key(press(KeyCode::PageDown)).unwrap();
}
assert_eq!(
app.help_scroll, metrics.max_scroll,
"the last row is reachable"
);
for _ in 0..50 {
app.handle_key(press(KeyCode::PageUp)).unwrap();
}
assert_eq!(app.help_scroll, 0, "and the first row stays the first");
}
#[test]
fn help_overlay_scrolls_its_rows_and_reopens_at_the_top() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let (_tmp, mut app) = make_app();
let entries = crate::ui::commandbar::help_overlay(&app.bindings, app.top_mode);
assert!(
entries.len() > 12,
"the overlay must be longer than the frame"
);
let draw = |app: &App| {
let mut terminal = Terminal::new(TestBackend::new(80, 12)).unwrap();
terminal.draw(|f| render(app, f, f.area())).unwrap();
let buf = terminal.backend().buffer();
let mut text = String::new();
for y in 0..buf.area.height {
for x in 0..buf.area.width {
text.push_str(buf[(x, y)].symbol());
}
text.push('\n');
}
text
};
app.open_help(Mode::Board);
let first = draw(&app);
assert!(
first.contains(&entries[0].0),
"the top of the list is on screen: {first}"
);
app.handle_key(press(KeyCode::PageDown)).unwrap();
let second = draw(&app);
assert_ne!(
second, first,
"scrolling a page must change what is drawn, not just a counter"
);
let page = app.help_page() as usize;
assert!(
second.contains(&entries[page].0),
"the row one page down is now visible: {second}"
);
assert!(
!second.contains(&entries[0].0),
"and the first page scrolled away"
);
app.close_help();
app.open_help(Mode::Board);
assert_eq!(app.help_scroll, 0);
assert!(draw(&app).contains(&entries[0].0));
}
#[test]
fn help_overlay_search_filters_the_list() {
let (_tmp, mut app) = make_app();
let entries = crate::ui::commandbar::help_overlay(&app.bindings, app.top_mode);
let first_key = entries[0].0.clone();
app.open_help(Mode::Board);
assert!(
draw_help(&app, 80, 24).contains(&first_key),
"the unfiltered list starts at the top"
);
app.handle_key(press(KeyCode::Char('/'))).unwrap();
assert!(app.help_searching, "the input line has the keyboard");
for c in "clone".chars() {
app.handle_key(press(KeyCode::Char(c))).unwrap();
}
assert_eq!(app.help_query, "clone");
let filtered = draw_help(&app, 80, 24);
assert!(
filtered.contains("clone issue"),
"the match is listed: {filtered}"
);
assert!(
filtered.contains("of") && filtered.contains("bindings"),
"and the summary says how many rows the filter kept: {filtered}"
);
assert!(
!filtered.contains("open detail"),
"an unrelated binding is filtered out: {filtered}"
);
app.handle_key(press(KeyCode::Enter)).unwrap();
assert!(!app.help_searching);
assert_eq!(app.help_query, "clone");
assert!(draw_help(&app, 80, 24).contains("clone issue"));
app.handle_key(press(KeyCode::Char('/'))).unwrap();
app.handle_key(press(KeyCode::Backspace)).unwrap();
app.handle_key(press(KeyCode::Backspace)).unwrap();
assert_eq!(app.help_query, "clo");
assert!(
draw_help(&app, 80, 24).contains("clone issue"),
"still a match"
);
app.handle_key(press(KeyCode::Esc)).unwrap();
assert!(!app.help_searching);
assert_eq!(app.help_query, "");
assert!(
matches!(app.mode, Mode::Help { .. }),
"clearing a search is not closing the help"
);
assert!(
draw_help(&app, 80, 24).contains(&first_key),
"the whole list is back"
);
app.handle_key(press(KeyCode::Esc)).unwrap();
assert!(!matches!(app.mode, Mode::Help { .. }));
}
#[test]
fn help_overlay_search_is_fuzzy() {
let (_tmp, mut app) = make_app();
app.open_help(Mode::Board);
app.handle_key(press(KeyCode::Char('/'))).unwrap();
for c in "nwiss".chars() {
app.handle_key(press(KeyCode::Char(c))).unwrap();
}
let text = draw_help(&app, 80, 24);
assert!(
text.contains("new issue"),
"a non-contiguous query still matches: {text}"
);
}
#[test]
fn help_overlay_search_resets_when_reopened() {
let (_tmp, mut app) = make_app();
app.open_help(Mode::Board);
app.handle_key(press(KeyCode::Char('/'))).unwrap();
app.handle_key(press(KeyCode::Char('x'))).unwrap();
app.handle_key(press(KeyCode::Enter)).unwrap();
assert_eq!(app.help_query, "x");
app.close_help();
app.open_help(Mode::Board);
assert!(app.help_query.is_empty(), "the query did not survive");
assert!(!app.help_searching);
}
#[test]
fn help_overlay_search_puts_the_caret_on_the_input_line() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let (_tmp, mut app) = make_app();
app.open_help(Mode::Board);
app.handle_key(press(KeyCode::Char('/'))).unwrap();
for c in "clone".chars() {
app.handle_key(press(KeyCode::Char(c))).unwrap();
}
let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
terminal.draw(|f| render(&app, f, f.area())).unwrap();
let cursor = terminal.backend().cursor_position();
let buf = terminal.backend().buffer();
let mut found = None;
for y in 0..buf.area.height {
let mut row = String::new();
for x in 0..buf.area.width {
row.push_str(buf[(x, y)].symbol());
}
if let Some(byte) = row.find("/clone") {
let column = row[..byte].chars().count() as u16 + "/clone".chars().count() as u16;
found = Some((y, column));
}
}
let (row, column) = found.expect("the prompt is drawn");
assert_eq!(
cursor,
ratatui::layout::Position::new(column, row),
"the prompt row was: {:?}",
{
let mut text = String::new();
for x in 0..buf.area.width {
text.push_str(buf[(x, row)].symbol());
}
text
}
);
}
#[test]
fn help_overlay_search_accepts_the_keys_that_close_the_overlay() {
let (_tmp, mut app) = make_app();
app.open_help(Mode::Board);
app.handle_key(press(KeyCode::Char('/'))).unwrap();
for c in "quit?".chars() {
app.handle_key(press(KeyCode::Char(c))).unwrap();
}
assert_eq!(app.help_query, "quit?");
assert!(app.help_searching, "still typing");
assert!(
matches!(app.mode, Mode::Help { .. }),
"neither `q` nor `?` closed the overlay"
);
app.handle_key(press(KeyCode::Enter)).unwrap();
app.handle_key(press(KeyCode::Char('q'))).unwrap();
assert!(!matches!(app.mode, Mode::Help { .. }));
}
#[test]
fn help_opens_from_board_and_closes() {
let (_tmp, mut app) = make_app();
let _ = app.handle_key(press(KeyCode::Char('?'))).unwrap();
assert!(
matches!(&app.mode, Mode::Help { return_to } if **return_to == Mode::Board),
"got {:?}",
app.mode
);
let _ = app.handle_key(press(KeyCode::Char('q'))).unwrap();
assert!(matches!(app.mode, Mode::Board));
}
#[test]
fn help_from_detail_returns_to_detail() {
let (tmp, mut app) = make_app();
let dir = tmp.path().join("issues");
write_issue(&dir, "2026-01-01-a.md", 1, "A", "todo", None);
let config = app.store.config.clone();
app.board = BoardState::build(&app.store, config.default_config());
open_detail(&mut app);
let _ = app.handle_key(press(KeyCode::Char('?'))).unwrap();
assert!(
matches!(&app.mode, Mode::Help { return_to } if **return_to == Mode::Detail),
"got {:?}",
app.mode
);
let _ = app.handle_key(press(KeyCode::Esc)).unwrap();
assert!(matches!(app.mode, Mode::Detail));
}
#[test]
fn help_overlay_click_closes_via_recorded_region() {
let (_tmp, mut app) = make_app();
let _ = app.handle_key(press(KeyCode::Char('?'))).unwrap();
assert!(matches!(app.mode, Mode::Help { .. }));
let _ = draw_board(&app, 80, 24);
let action = app.handle_mouse(down_left(20, 5)).unwrap();
assert_eq!(action, Action::HelpClose);
app.close_help();
assert!(matches!(app.mode, Mode::Board));
}
#[test]
fn help_in_cycle_mode_returns_to_the_cycle_mode() {
let (_tmp, mut app) = make_cycle_app();
app.handle_key(press(KeyCode::F(4))).unwrap();
app.handle_key(press(KeyCode::Char('?'))).unwrap();
assert!(matches!(app.mode, Mode::Help { .. }));
app.handle_key(press(KeyCode::Esc)).unwrap();
assert!(
matches!(app.mode, Mode::Cycle),
"closing help returns to the Cycle mode, got {:?}",
app.mode
);
}