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 opening_an_issue_with_a_mention_shows_the_notice() {
    let (_tmp, mut app, number) = app_with_mention("blocked on @alice");
    app.open_issue_by_number(number).unwrap();
    assert!(matches!(app.mode, Mode::MentionNotice));
    let view = app.mentions.as_ref().expect("the notice is open");
    assert_eq!(view.rows.len(), 1);
    assert_eq!(view.rows[0].body, "alice");
    assert!(
        view.rows[0].context.contains("@alice"),
        "{:?}",
        view.rows[0].context
    );
    // …and it is drawn.
    let text = frame_text(&app, 100, 20);
    assert!(text.contains("Mentions for you"), "{text}");
    assert!(
        text.contains("@alice") || text.contains("blocked on @alice"),
        "{text}"
    );
}

#[test]
fn an_issue_with_no_mentions_for_the_user_opens_no_notice() {
    let (_tmp, mut app, number) = app_with_mention("cc @someone-else");
    app.open_issue_by_number(number).unwrap();
    assert!(
        matches!(app.mode, Mode::Detail),
        "no notice for a stranger's mention"
    );
    assert!(app.mentions.is_none());
}

#[test]
fn ignoring_a_mention_writes_nothing_and_shows_it_again() {
    let (_tmp, mut app, number) = app_with_mention("blocked on @alice");
    app.open_issue_by_number(number).unwrap();
    app.handle_key(press(KeyCode::Char('i'))).unwrap();
    assert!(
        matches!(app.mode, Mode::Detail),
        "ignore returns to the pane"
    );
    assert!(app.mentions.is_none());
    // Nothing was written: the mention is still unread…
    recount_mentions(&mut app);
    assert_eq!(app.unread_mentions, 1);
    // …so the next open shows it again.
    app.open_issue_by_number(number).unwrap();
    assert!(matches!(app.mode, Mode::MentionNotice));
}

#[test]
fn marking_a_mention_read_persists_and_is_not_shown_again() {
    let (_tmp, mut app, number) = app_with_mention("blocked on @alice");
    app.open_issue_by_number(number).unwrap();
    app.handle_key(press(KeyCode::Char('r'))).unwrap();
    assert!(
        app.mentions.is_none(),
        "the last pending mention closes the notice"
    );
    assert!(matches!(app.mode, Mode::Detail));
    assert_eq!(app.unread_mentions, 0, "the count is recomputed");
    app.open_issue_by_number(number).unwrap();
    assert!(
        matches!(app.mode, Mode::Detail),
        "a read mention is not shown again"
    );
}

#[test]
fn jumping_to_a_mention_scrolls_the_detail_pane_to_it() {
    // Six filler lines put the mention on body line 6.
    let body = "one\ntwo\nthree\nfour\nfive\nsix\nblocked on @alice\n";
    let (_tmp, mut app, number) = app_with_mention(body);
    app.open_issue_by_number(number).unwrap();
    let detail = app.detail.as_mut().expect("a detail pane");
    // What the renderer normally records; without it every scroll clamps to zero.
    detail.max_scroll.set(50);
    detail.scroll_offset = 0;
    app.handle_key(press(KeyCode::Enter)).unwrap();
    assert!(app.mentions.is_none(), "the notice closes on jump");
    assert_eq!(
        app.detail.as_ref().unwrap().scroll_offset,
        6,
        "scrolled to the mention"
    );
}

#[test]
fn the_indicator_row_appears_with_unread_mentions_and_goes_when_read() {
    let (_tmp, mut app, number) = app_with_mention("blocked on @alice");
    assert_eq!(mention_row_height(&app), 1);
    let text = frame_text(&app, 100, 20);
    assert!(text.contains("1 unread mention"), "{text}");
    assert!(text.contains("press i to open"), "{text}");

    // Mark it read from the view, and the row is gone.
    app.handle_key(press(KeyCode::Char('i'))).unwrap();
    assert!(matches!(app.mode, Mode::Mentions));
    app.handle_key(press(KeyCode::Char('r'))).unwrap();
    assert_eq!(app.unread_mentions, 0);
    assert_eq!(mention_row_height(&app), 0);
    let text = frame_text(&app, 100, 20);
    assert!(!text.contains("unread mention"), "{text}");
    let _ = number;
}

#[test]
fn no_unread_mentions_means_no_indicator_row() {
    let (_tmp, app, _number) = app_with_mention("nothing addressed here");
    assert_eq!(mention_row_height(&app), 0);
    let text = frame_text(&app, 100, 20);
    assert!(!text.contains("unread mention"), "{text}");
}

#[test]
fn the_board_chord_opens_the_mentions_view_across_issues() {
    let (_tmp, mut app) = make_app();
    let first = seed_issue(&mut app, "blocked on @alice");
    let _second = seed_issue(&mut app, "also blocked on @alice");
    app.mention_candidates = vec!["alice".to_string()];
    recount_mentions(&mut app);
    assert_eq!(app.unread_mentions, 2);

    app.handle_key(press(KeyCode::Char('i'))).unwrap();
    assert!(
        matches!(app.mode, Mode::Mentions),
        "the chord opens the view"
    );
    let rows = app
        .mentions_view
        .as_ref()
        .expect("the view is open")
        .rows
        .len();
    assert_eq!(rows, 2, "both issues are listed");
    let text = frame_text(&app, 100, 20);
    assert!(text.contains("Pending mentions"), "{text}");

    // Selecting a row opens that issue at its mention.
    app.handle_key(press(KeyCode::Enter)).unwrap();
    assert!(matches!(app.mode, Mode::Detail));
    assert!(
        app.mentions.is_none(),
        "the opened issue's own notice does not preempt"
    );
    assert_eq!(app.detail.as_ref().unwrap().issue.number, first);
}

#[test]
fn the_mentions_view_reports_nothing_to_show_rather_than_opening_empty() {
    let (_tmp, mut app, _number) = app_with_mention("nothing addressed here");
    app.handle_key(press(KeyCode::Char('i'))).unwrap();
    assert!(matches!(app.mode, Mode::Board));
    assert!(app.mentions_view.is_none());
    assert_eq!(app.status_msg.as_deref(), Some("no pending mentions"));
}

#[test]
fn mentions_disabled_means_no_notice_no_count_and_no_indicator() {
    let (_tmp, mut app, number) = app_with_mention("blocked on @alice");
    assert_eq!(app.unread_mentions, 1, "the mention is there to begin with");
    app.store.config.mentions.enabled = false;
    app.mention_candidates = vec!["alice".to_string()];
    recount_mentions(&mut app);
    assert_eq!(app.unread_mentions, 0);
    assert_eq!(mention_row_height(&app), 0);
    app.open_issue_by_number(number).unwrap();
    assert!(
        matches!(app.mode, Mode::Detail),
        "no notice when the feature is off"
    );
    // The chord says why there is nothing, rather than implying an empty queue. It is
    // a **board** chord, so the test has to be on the board — the detail pane binds `i`
    // to nothing.
    app.mode = Mode::Board;
    app.handle_key(press(KeyCode::Char('i'))).unwrap();
    assert!(matches!(app.mode, Mode::Board));
    assert!(
        app.status_msg
            .as_deref()
            .is_some_and(|m| m.contains("disabled")),
        "{:?}",
        app.status_msg
    );
}