tix-tracker 0.12.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::*;
use std::fs;

#[test]
fn mouse_drag_updates_preview_column_while_held() {
    let (_tmp, mut app) = make_mouse_app();
    let (sx, sy) = card_cell(&app, 0, 1);
    let target = column_cell(&app, 1);
    app.handle_mouse(down_left(sx, sy)).unwrap();
    app.handle_mouse(drag_event(target.0, target.1)).unwrap();
    assert_eq!(app.drag_preview_col, Some(1));
    assert_eq!(app.board.hover, None);
    app.handle_mouse(up_left(target.0, target.1)).unwrap();
    assert_eq!(app.drag_preview_col, None);
}

#[test]
fn action_menu_view_issue_matches_the_board_viewer_key() {
    let (_tmp, mut app) = make_mouse_app();
    let (x, y) = card_cell(&app, 0, 1);
    app.handle_mouse(down_right(x, y)).unwrap();
    assert!(select_menu_action(&mut app, &IssueAction::ViewIssue));
    let action = app.handle_key(press(KeyCode::Enter)).unwrap();

    let selected = app.board.get_selected_card().cloned().unwrap().filename;
    assert_eq!(
        action,
        Action::OpenViewer(selected.clone()),
        "menu View issue returns OpenViewer for the selected card"
    );

    let (_tmp, mut app) = make_mouse_app();
    let (x, y) = card_cell(&app, 0, 1);
    let _ = app.handle_mouse(down_left(x, y)).unwrap();
    let key_action = app.handle_key(press(KeyCode::Char('V'))).unwrap();
    let key_filename = match app.board.get_selected_card() {
        Some(card) => card.filename.clone(),
        None => String::new(),
    };
    assert_eq!(
        key_action,
        Action::OpenViewer(key_filename),
        "board 'V' returns OpenViewer for the same card"
    );

    // And plain `v` is the saved-views modal on the board: the collision is
    // resolved in favour of views, not merely relocated. `V` is where #1507
    // moved, and the viewer keeps `v` in the Detail context.
    let (_tmp, mut app) = make_mouse_app();
    let (x, y) = card_cell(&app, 0, 1);
    let _ = app.handle_mouse(down_left(x, y)).unwrap();
    let _ = app.handle_key(press(KeyCode::Char('v'))).unwrap();
    assert!(
        matches!(app.mode, Mode::Views),
        "board 'v' opens the saved-views modal, got {:?}",
        app.mode
    );
}

#[test]
fn action_menu_names_mdterm_in_the_view_issue_caption() {
    let (_tmp, mut app) = make_mouse_app();
    app.store.config.viewer = Some("mdterm".to_string());
    let (x, y) = card_cell(&app, 0, 1);
    app.handle_mouse(down_right(x, y)).unwrap();
    let label = app
        .action_menu
        .as_ref()
        .and_then(|menu| {
            menu.items()
                .iter()
                .find(|item| item.action == IssueAction::ViewIssue)
        })
        .map(|item| item.label.clone());
    assert_eq!(label.as_deref(), Some("View issue (in mdterm)"));

    let (_tmp, mut app) = make_mouse_app();
    app.store.config.viewer = Some("less".to_string());
    let (x, y) = card_cell(&app, 0, 1);
    app.handle_mouse(down_right(x, y)).unwrap();
    let label = app
        .action_menu
        .as_ref()
        .and_then(|menu| {
            menu.items()
                .iter()
                .find(|item| item.action == IssueAction::ViewIssue)
        })
        .map(|item| item.label.clone());
    assert_eq!(
        label.as_deref(),
        Some("View issue"),
        "a pinned non-mdterm viewer is not captioned as mdterm"
    );
}

#[test]
fn detail_v_returns_open_viewer_for_the_open_issue() {
    let (_tmp, mut app) = make_mouse_app();
    let (x, y) = card_cell(&app, 0, 1);
    let _ = app.handle_mouse(down_left(x, y)).unwrap();
    let _ = app.handle_mouse(up_left(x, y)).unwrap();
    assert!(select_menu_action(&mut app, &IssueAction::OpenDetail));
    let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
    let number = app.detail.as_ref().unwrap().issue.number;

    let action = app.handle_key(press(KeyCode::Char('v'))).unwrap();
    let Action::OpenViewer(filename) = action else {
        panic!("detail 'v' must return OpenViewer");
    };
    let issue = app.store.get(IssueSelector::Filename(filename)).unwrap();
    assert_eq!(issue.number, number, "the viewer opens the detail's issue");
}

#[test]
fn config_viewer_row_persists_and_clears_back_to_auto() {
    let (tmp, mut app) = make_app_with_config_stores();
    open_config(&mut app);
    // The viewer row is the second general row: after the display rows and
    // the editor row.
    let viewer_row =
        app.active_board_config().columns.len() + crate::ui::settings::DISPLAY_ROWS + 1;
    // The rail starts focused; `l` twice drills into the selected board's
    // detail, whose cursor then points at the viewer row.
    let _ = app.handle_key(press(KeyCode::Right)).unwrap();
    let _ = app.handle_key(press(KeyCode::Right)).unwrap();
    app.settings.as_mut().unwrap().detail_selected = viewer_row;
    let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
    assert!(
        matches!(
            app.settings.as_ref().unwrap().prompt,
            Some(SettingsPrompt::SetViewer(_))
        ),
        "Enter on the viewer row opens the prompt"
    );
    for c in "mdterm".chars() {
        let _ = app.handle_key(press(KeyCode::Char(c))).unwrap();
    }
    let _ = app.handle_key(press(KeyCode::Enter)).unwrap();

    assert_eq!(app.store.config.viewer.as_deref(), Some("mdterm"));
    let toml = fs::read_to_string(tmp.path().join("tix.toml")).unwrap();
    assert!(
        toml.contains(r#"viewer = "mdterm""#),
        "the viewer persists to tix.toml:\n{toml}"
    );

    // Reopen the row (still selected, body still focused) and clear the
    // seeded value: a blank submission returns the key to the auto chain.
    let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
    for _ in 0.."mdterm".len() {
        let _ = app.handle_key(press(KeyCode::Backspace)).unwrap();
    }
    let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
    assert_eq!(app.store.config.viewer, None);
    let toml = fs::read_to_string(tmp.path().join("tix.toml")).unwrap();
    assert!(
        !toml.contains("viewer"),
        "a blank value clears back to auto:\n{toml}"
    );
}