finery 0.12.0

Keyboard-first Jira backlog and Composer with optional MCP access
Documentation
use ratatui::style::Modifier;

use super::{
    ChangeBadge, TicketRowDetails, WorkItemKind, WorkItemRow, attachment_summary_text,
    mermaid_diagram_summary_text, story_points_label, ticket_menu_max_height, ticket_summary_text,
    work_item_title_with_key_line_with_match,
};

#[test]
fn ticket_menu_height_cap_uses_sixty_percent_of_the_viewport() {
    assert_eq!(ticket_menu_max_height(50), 30);
}

#[test]
fn mermaid_rows_capitalize_the_diagram_type() {
    tuicore::init();

    let text = mermaid_diagram_summary_text("Lifecycle", "state", false, false, false);

    assert_eq!(text.lines[0].spans[2].content, "");
    assert_eq!(text.lines[0].spans.last().unwrap().content, "State");
}

#[test]
fn published_mermaid_rows_show_the_published_state() {
    tuicore::init();

    let text = mermaid_diagram_summary_text("Lifecycle", "state", false, true, true);

    assert_eq!(text.lines[0].spans.last().unwrap().content, " • published");
    assert!(
        text.lines[0]
            .spans
            .iter()
            .all(|span| span.style.fg == Some(tuicore::theme().muted_fg()))
    );
}

#[test]
fn search_matches_are_underlined_in_the_shared_ticket_title_template() {
    tuicore::init();
    let row = WorkItemRow {
        id: "KAN-2".into(),
        key: "KAN-2".into(),
        title: "Legacy in-progress placeholder".into(),
        kind: WorkItemKind::Story,
        priority: "Low".into(),
        status: "In Progress".into(),
        done: false,
        assignee: "Marlo".into(),
        labels: Vec::new(),
        story_points: None,
        show_story_points: false,
        story_points_estimated: false,
        story_points_from_average: false,
        change_badge: None,
        submitted: false,
    };

    let line = work_item_title_with_key_line_with_match(&row, None, Some("legacy"));
    let matched = line
        .spans
        .iter()
        .find(|span| span.content == "Legacy")
        .expect("the matching title text is rendered separately");

    assert!(matched.style.add_modifier.contains(Modifier::UNDERLINED));
}

#[test]
fn done_ticket_keys_are_struck_through_in_shared_ticket_rows() {
    tuicore::init();
    let row = WorkItemRow {
        id: "KAN-22".into(),
        key: "KAN-22".into(),
        title: "Completed ticket".into(),
        kind: WorkItemKind::Story,
        priority: "Low".into(),
        status: "Done".into(),
        done: true,
        assignee: "Marlo".into(),
        labels: Vec::new(),
        story_points: None,
        show_story_points: false,
        story_points_estimated: false,
        story_points_from_average: false,
        change_badge: None,
        submitted: false,
    };

    let line = work_item_title_with_key_line_with_match(&row, None, None);
    let key = line
        .spans
        .iter()
        .find(|span| span.content == "KAN-22")
        .expect("the ticket key is rendered separately");

    assert!(key.style.add_modifier.contains(Modifier::CROSSED_OUT));
}

#[test]
fn case_insensitive_done_statuses_strike_through_ticket_keys() {
    tuicore::init();
    let row = WorkItemRow {
        id: "KAN-23".into(),
        key: "KAN-23".into(),
        title: "Lowercase done ticket".into(),
        kind: WorkItemKind::Story,
        priority: "Low".into(),
        status: "done".into(),
        done: crate::store::work_items::is_done_status("done"),
        assignee: "Marlo".into(),
        labels: Vec::new(),
        story_points: None,
        show_story_points: false,
        story_points_estimated: false,
        story_points_from_average: false,
        change_badge: None,
        submitted: false,
    };

    let line = work_item_title_with_key_line_with_match(&row, None, None);
    let key = line
        .spans
        .iter()
        .find(|span| span.content == "KAN-23")
        .expect("the ticket key is rendered separately");

    assert!(key.style.add_modifier.contains(Modifier::CROSSED_OUT));
}

#[test]
fn average_derived_story_points_show_one_decimal_place() {
    let row = WorkItemRow {
        id: "KAN-22".into(),
        key: "KAN-22".into(),
        title: "Estimated ticket".into(),
        kind: WorkItemKind::Story,
        priority: "Low".into(),
        status: "To Do".into(),
        done: false,
        assignee: "Marlo".into(),
        labels: Vec::new(),
        story_points: Some(3.932_423_532),
        show_story_points: true,
        story_points_estimated: true,
        story_points_from_average: true,
        change_badge: None,
        submitted: false,
    };

    assert_eq!(story_points_label(&row), "~3.9");
}

#[test]
fn ticket_annotations_extend_composer_metadata_without_hiding_change_state() {
    tuicore::init();
    let row = WorkItemRow {
        id: "FIN-123".into(),
        key: "FIN-123".into(),
        title: "Move ticket".into(),
        kind: WorkItemKind::Story,
        priority: "High".into(),
        status: "In Progress".into(),
        done: false,
        assignee: "Marlo".into(),
        labels: vec!["AB".into(), "CD".into(), "Refinery".into()],
        story_points: None,
        show_story_points: false,
        story_points_estimated: false,
        story_points_from_average: false,
        change_badge: Some(ChangeBadge::Modified),
        submitted: true,
    };

    let text = ticket_summary_text(
        &row,
        None,
        None,
        TicketRowDetails {
            subtask_progress: None,
            fix_versions: &["v0.5".into()],
            epic_name: Some("Ticket metadata"),
            annotation: Some("FIN-100 → FIN-200"),
        },
    );
    let metadata = text.lines[1]
        .spans
        .iter()
        .map(|span| span.content.as_ref())
        .collect::<String>();

    assert!(metadata.contains("M"));
    assert!(metadata.contains("AB|CD|Refinery"));
    assert!(metadata.find("In Progress") < metadata.find("AB|CD|Refinery"));
    assert!(metadata.find("Ticket metadata") < metadata.find("v0.5"));
    assert!(metadata.contains("submitted"));
    assert!(metadata.contains("FIN-100 → FIN-200"));
}

#[test]
fn long_labels_use_a_tight_chip_with_an_overflow_count_after_status() {
    tuicore::init();
    let row = WorkItemRow {
        id: "FIN-123".into(),
        key: "FIN-123".into(),
        title: "Keep labels compact".into(),
        kind: WorkItemKind::Story,
        priority: "High".into(),
        status: "To Do".into(),
        done: false,
        assignee: "Marlo".into(),
        labels: vec![
            "backlog-rank-03".into(),
            "finery-runway-seed-v1".into(),
            "northstar-commerce".into(),
            "seed-backlog".into(),
        ],
        story_points: None,
        show_story_points: false,
        story_points_estimated: false,
        story_points_from_average: false,
        change_badge: None,
        submitted: false,
    };

    let text = ticket_summary_text(
        &row,
        None,
        None,
        TicketRowDetails {
            subtask_progress: None,
            fix_versions: &[],
            epic_name: None,
            annotation: None,
        },
    );
    let metadata = text.lines[1]
        .spans
        .iter()
        .map(|span| span.content.as_ref())
        .collect::<String>();

    assert!(metadata.contains("backlog-r…|+3"));
    assert!(metadata.find("To Do") < metadata.find("backlog-r…|+3"));
}

#[test]
fn attachment_rows_show_filename_date_and_size() {
    tuicore::init();

    let text = attachment_summary_text(
        crate::store::composer::AttachmentChangeKind::Synced,
        "image-20260904-161404.png",
        "2026-09-04T16:14:04.000+0000",
        21_504,
        false,
        false,
    );
    let rendered = text.lines[0]
        .spans
        .iter()
        .map(|span| span.content.as_ref())
        .collect::<String>();

    assert!(rendered.contains("S "));
    assert!(rendered.contains("image-20260904-161404.png"));
    assert!(rendered.contains("Sep 04, 2026"));
    assert!(rendered.contains("21 KB"));
}

#[test]
fn highlighted_attachment_rows_use_the_selected_foreground() {
    tuicore::init();

    let text = attachment_summary_text(
        crate::store::composer::AttachmentChangeKind::Synced,
        "design.png",
        "2026-09-04T16:14:04.000+0000",
        21_504,
        true,
        false,
    );

    assert!(
        text.lines[0]
            .spans
            .iter()
            .all(|span| span.style.fg == Some(tuicore::theme().selected_fg()))
    );
}