tix-tracker 0.11.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
#[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());
}

/// The pane's page keys move a **page**, not a handful of lines, and stop at the
/// end of the content instead of scrolling past it (#1442).
#[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);
    }
    // A page keeps one row of the previous page, so the reader has an anchor.
    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, 19);
    app.handle_key(press(KeyCode::PageDown)).unwrap();
    assert_eq!(app.detail.as_ref().unwrap().scroll_offset, 38);
    // 38 + 19 = 57 > 45, so this one lands on the last page rather than past it.
    app.handle_key(press(KeyCode::PageDown)).unwrap();
    assert_eq!(app.detail.as_ref().unwrap().scroll_offset, 45);
    app.handle_key(press(KeyCode::PageUp)).unwrap();
    assert_eq!(app.detail.as_ref().unwrap().scroll_offset, 26);
}

#[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"
    );
}

/// `l` and `→` open the focused row exactly like `Enter`/a click and move the
/// keyboard into the opened content; `Esc` leaves the rail without opening.
#[test]
fn rail_right_and_l_activate_the_focused_row() {
    // `l` on the `release` limb switches to that board and returns.
    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"
    );

    // `→` runs the same transition (here the already-active `default` board).
    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");

    // `Esc` leaves the rail without activating anything.
    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);
}

/// Entering the rail with `h`/`Left` lands the cursor on the active mode's
/// trunk; the cursor highlight must outrank the active-mode colour so the
/// position is always visible.
#[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");
    // Board is the active mode and starts at its left edge, so `h` enters the
    // rail on the Board trunk — the row that would otherwise draw active-cyan.
    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"
    );
}

/// A focused rail draws a right-edge focus accent; the unfocused rail does not.
#[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"
    );
}