#[allow(clippy::wildcard_imports)]
use super::super::*;
#[allow(clippy::wildcard_imports)]
use super::helpers::*;
#[test]
fn detail_esc_back_to_board() {
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::Esc)).unwrap();
assert!(matches!(app.mode, Mode::Board));
assert!(app.detail.is_none());
}
#[test]
fn detail_page_keys_scroll_one_page_and_stop_at_the_end() {
let (_tmp, mut app) = make_mouse_app();
let (x, y) = card_cell(&app, 0, 0);
app.handle_mouse(down_left(x, y)).unwrap();
app.handle_mouse(up_left(x, y)).unwrap();
app.handle_mouse(down_left(x, y)).unwrap();
assert!(matches!(app.mode, Mode::Detail));
if let Some(d) = app.detail.as_mut() {
d.viewport.set(20);
d.max_scroll.set(45);
}
let page = app.detail.as_ref().unwrap().page();
assert_eq!(page, 19);
app.handle_key(press(KeyCode::PageDown)).unwrap();
assert_eq!(app.detail.as_ref().unwrap().scroll_offset.get(), 19);
app.handle_key(press(KeyCode::PageDown)).unwrap();
assert_eq!(app.detail.as_ref().unwrap().scroll_offset.get(), 38);
app.handle_key(press(KeyCode::PageDown)).unwrap();
assert_eq!(app.detail.as_ref().unwrap().scroll_offset.get(), 45);
app.handle_key(press(KeyCode::PageUp)).unwrap();
assert_eq!(app.detail.as_ref().unwrap().scroll_offset.get(), 26);
}
#[test]
fn detail_body_scrolls_by_row_and_jumps_to_the_ends() {
let (_tmp, mut app) = make_mouse_app();
let (x, y) = card_cell(&app, 0, 0);
app.handle_mouse(down_left(x, y)).unwrap();
app.handle_mouse(up_left(x, y)).unwrap();
app.handle_mouse(down_left(x, y)).unwrap();
assert!(matches!(app.mode, Mode::Detail));
if let Some(d) = app.detail.as_mut() {
d.viewport.set(20);
d.max_scroll.set(45);
}
let offset = |app: &App| app.detail.as_ref().unwrap().scroll_offset.get();
app.handle_key(press(KeyCode::Char('j'))).unwrap();
assert_eq!(offset(&app), 1, "j scrolls down a row");
app.handle_key(press(KeyCode::Down)).unwrap();
assert_eq!(offset(&app), 2, "Down scrolls down a row");
app.handle_key(press(KeyCode::Char('k'))).unwrap();
assert_eq!(offset(&app), 1, "k scrolls up a row");
app.handle_key(press(KeyCode::Up)).unwrap();
assert_eq!(offset(&app), 0, "Up scrolls up a row");
app.handle_key(press(KeyCode::Char('k'))).unwrap();
assert_eq!(offset(&app), 0, "and clamps at the top");
app.handle_key(press(KeyCode::Char('G'))).unwrap();
assert_eq!(offset(&app), 45, "G jumps to the bottom");
app.handle_key(press(KeyCode::Char('g'))).unwrap();
assert_eq!(offset(&app), 0, "g jumps back to the top");
}
#[test]
fn resolve_render_size_falls_back_for_non_tty() {
assert_eq!(
resolve_render_size(None),
(RENDER_FALLBACK_WIDTH, RENDER_FALLBACK_HEIGHT)
);
assert_eq!(resolve_render_size(Some((0, 0))), (1, 1));
}
#[test]
fn navigation_toggle_cycles_display_states() {
let (_tmp, mut app) = make_app();
assert_eq!(app.nav_display, NavDisplay::Default);
let toggle = |app: &mut App| {
app.handle_key(press_with_modifiers(
KeyCode::Char('n'),
crossterm::event::KeyModifiers::CONTROL,
))
};
toggle(&mut app).unwrap();
assert_eq!(app.nav_display, NavDisplay::Off);
toggle(&mut app).unwrap();
assert_eq!(app.nav_display, NavDisplay::Small);
toggle(&mut app).unwrap();
assert_eq!(app.nav_display, NavDisplay::Default);
}
#[test]
fn navigation_renders_in_all_three_display_states() {
let (_tmp, mut app) = make_app();
app.nav_display = NavDisplay::Default;
let cells = draw_board(&app, 80, 24);
assert!(
flush_line(&cells, 80, 0).starts_with("F1 New Issue"),
"default navigation shows the full label, got {:?}",
flush_line(&cells, 80, 0)
);
app.nav_display = NavDisplay::Small;
let cells = draw_board(&app, 80, 24);
let row = flush_line(&cells, 80, 0);
assert!(
row.starts_with("F1 ") && !row.starts_with("F1 New Issue"),
"small navigation shows only the chord, got {row:?}"
);
app.nav_display = NavDisplay::Off;
let cells = draw_board(&app, 80, 24);
assert!(
!flush_line(&cells, 80, 0).starts_with("F1"),
"off navigation draws nothing in the left column"
);
}
#[test]
fn rail_right_and_l_activate_the_focused_row() {
let (_tmp, mut app) = make_branchy_app();
let board_trunk = app
.nav_rows()
.iter()
.position(|action| *action == Action::SwitchMode(TopMode::Board))
.expect("the Board trunk is a rail row");
let _ = app.handle_key(press(KeyCode::Char('h'))).unwrap();
let _ = app.handle_key(press(KeyCode::Char('j'))).unwrap();
let _ = app.handle_key(press(KeyCode::Char('j'))).unwrap();
assert_eq!(
app.nav_rows()[board_trunk + 2],
Action::SelectBoard("release".into())
);
let _ = app.handle_key(press(KeyCode::Char('l'))).unwrap();
assert_eq!(app.active_board_name(), "release");
assert_eq!(
app.nav_focus_index(),
None,
"`l` opens the row and hands the keyboard to the content"
);
let (_tmp, mut app) = make_branchy_app();
let _ = app.handle_key(press(KeyCode::Char('h'))).unwrap();
let _ = app.handle_key(press(KeyCode::Char('j'))).unwrap();
let _ = app.handle_key(press(KeyCode::Right)).unwrap();
assert_eq!(app.active_board_name(), "default");
assert_eq!(app.nav_focus_index(), None, "`→` opens the row too");
let (_tmp, mut app) = make_branchy_app();
let _ = app.handle_key(press(KeyCode::Char('h'))).unwrap();
let _ = app.handle_key(press(KeyCode::Esc)).unwrap();
assert_eq!(app.nav_focus_index(), None);
}
#[test]
fn the_rail_cursor_is_visible_on_the_active_trunk() {
let (_tmp, mut app) = make_branchy_app();
app.nav_display = NavDisplay::Default;
let board_trunk = app
.nav_rows()
.iter()
.position(|action| *action == Action::SwitchMode(TopMode::Board))
.expect("the Board trunk is a rail row");
let _ = app.handle_key(press(KeyCode::Char('h'))).unwrap();
assert_eq!(app.nav_focus_index(), Some(board_trunk));
let buffer = draw_buffer(&app, 80, 24);
assert_eq!(
buffer[(0, board_trunk as u16)].style().bg,
Some(Color::DarkGray),
"the cursor on the active trunk is highlighted, not left as active-cyan"
);
}
#[test]
fn the_focused_rail_draws_a_focus_accent() {
let (_tmp, mut app) = make_branchy_app();
app.nav_display = NavDisplay::Default;
let edge = app.nav_width() - 1;
let unfocused = draw_buffer(&app, 80, 24);
assert_ne!(
unfocused[(edge, 0)].style().fg,
Some(Color::Yellow),
"the unfocused rail draws no focus accent"
);
let _ = app.handle_key(press(KeyCode::Char('h'))).unwrap();
let focused = draw_buffer(&app, 80, 24);
assert_eq!(
focused[(edge, 0)].style().fg,
Some(Color::Yellow),
"the focused rail's right edge is accented"
);
}
#[test]
fn every_navigable_top_level_page_enters_the_rail_with_h_or_left() {
let modes = [
TopMode::Board,
TopMode::Cycle,
TopMode::Milestones,
TopMode::Repos,
TopMode::Stats,
TopMode::Schedule,
TopMode::Portfolio,
TopMode::Library,
TopMode::Config,
];
for mode in modes {
for key in [KeyCode::Char('h'), KeyCode::Left] {
let (_tmp, mut app) = make_app();
app.nav_display = NavDisplay::Default;
app.switch_top_mode(mode).unwrap();
app.handle_key(press(key)).unwrap();
assert!(
app.nav_focus_index().is_some(),
"{mode:?} did not hand {key:?} to the navigation rail"
);
}
}
}
#[test]
fn the_new_issue_form_keeps_h_as_text_and_uses_left_at_column_zero_for_navigation() {
let (_tmp, mut app) = make_app();
app.switch_top_mode(TopMode::NewIssue).unwrap();
app.handle_key(press(KeyCode::Char('h'))).unwrap();
assert_eq!(
app.form.as_ref().unwrap().title,
"h",
"a letter typed into the title remains text"
);
app.form.as_mut().unwrap().cursor_to_start();
app.handle_key(press(KeyCode::Left)).unwrap();
assert!(
app.nav_focus_index().is_some(),
"Left at column zero reaches the rail"
);
}