quinjet 0.0.45

A fast, live, keyboard-first Git source-control interface for the terminal
use super::*;
use crate::convert::cells;
use crate::tabs::{RepositoryTabs, TabId};

fn repository_tabs() -> (RepositoryTabs<()>, TabId, TabId, TabId) {
    let mut tabs = RepositoryTabs::new("one", "/one", ());
    let first = tabs.active_id().expect("initial tab is active");
    let second = tabs.append("two", "/two", ());
    let third = tabs.append("three", "/three", ());
    assert!(tabs.activate(first));
    (tabs, first, second, third)
}

fn app_with_repository_tabs(tabs: &RepositoryTabs<()>) -> App {
    let mut app = App::new("/one", "one");
    app.set_repository_tabs(tabs.infos());
    app.geometry.repository_tab_hits = app
        .repository_tabs
        .iter()
        .enumerate()
        .map(|(index, tab)| RepositoryTabHit {
            area: Rect::new(cells(index.saturating_mul(12)), 0, 12, 1),
            close: Rect::new(cells(index.saturating_mul(12).saturating_add(8)), 0, 3, 1),
            id: tab.id,
        })
        .collect();
    app.geometry.repository_tab_open = Rect::new(36, 0, 5, 1);
    app
}

const fn mouse(kind: MouseEventKind, column: u16, row: u16) -> MouseEvent {
    MouseEvent {
        kind,
        column,
        row,
        modifiers: KeyModifiers::NONE,
    }
}

fn project_group() -> ProjectGroup {
    ProjectGroup {
        name: "target".to_owned(),
        common_dir: PathBuf::from("/target/.git"),
        worktrees: vec![Worktree {
            path: PathBuf::from("/target"),
            head: "0123456789abcdef".to_owned(),
            updated_at: Some("2026-08-22T18:00:00Z".to_owned()),
            updated_unix: Some(1_776_964_800),
            branch: Some("main".to_owned()),
            current: false,
            bare: false,
            detached: false,
            locked: None,
            prunable: None,
        }],
    }
}

#[test]
fn project_shortcuts_choose_current_or_new_tab_mode() {
    let now = Instant::now();
    let mut app = App::new("/one", "one");
    app.project_groups = vec![project_group()];

    let effects = app.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE), now);
    assert!(effects.iter().any(|effect| matches!(
        effect,
        AppEffect::Git(command)
            if matches!(command.as_ref(), WorkerCommand::LoadRecentProjects { .. })
    )));
    assert!(matches!(
        app.modal,
        Some(Modal::Projects {
            mode: ProjectOpenMode::CurrentTab,
            ..
        })
    ));
    let effects = app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE), now);
    assert!(matches!(
        effects.as_slice(),
        [AppEffect::SwitchRepository(path)] if path == Path::new("/target")
    ));
    app.modal = None;

    let effects = app.handle_key(KeyEvent::new(KeyCode::Char('N'), KeyModifiers::SHIFT), now);
    assert!(effects.iter().any(|effect| matches!(
        effect,
        AppEffect::Git(command)
            if matches!(command.as_ref(), WorkerCommand::LoadRecentProjects { .. })
    )));
    assert!(matches!(
        app.modal,
        Some(Modal::Projects {
            mode: ProjectOpenMode::NewTab,
            ..
        })
    ));
    let effects = app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE), now);
    assert!(matches!(
        effects.as_slice(),
        [AppEffect::OpenRepositoryTab(path)] if path == Path::new("/target")
    ));
}

#[test]
fn control_tab_cycles_and_control_w_closes_the_active_repository_tab() {
    let (mut tabs, first, second, third) = repository_tabs();
    let mut app = app_with_repository_tabs(&tabs);
    let now = Instant::now();

    let effects = app.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::CONTROL), now);
    assert!(matches!(
        effects.as_slice(),
        [AppEffect::ActivateRepositoryTab(id)] if *id == second
    ));

    let effects = app.handle_key(
        KeyEvent::new(KeyCode::Tab, KeyModifiers::CONTROL | KeyModifiers::SHIFT),
        now,
    );
    assert!(matches!(
        effects.as_slice(),
        [AppEffect::ActivateRepositoryTab(id)] if *id == third
    ));

    let effects = app.handle_key(KeyEvent::new(KeyCode::BackTab, KeyModifiers::CONTROL), now);
    assert!(matches!(
        effects.as_slice(),
        [AppEffect::ActivateRepositoryTab(id)] if *id == third
    ));

    let effects = app.handle_key(
        KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL),
        now,
    );
    assert!(matches!(
        effects.as_slice(),
        [AppEffect::CloseRepositoryTab(id)] if *id == first
    ));

    assert!(tabs.activate(second));
    app.set_repository_tabs(tabs.infos());
    let effects = app.handle_key(
        KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL),
        now,
    );
    assert!(matches!(
        effects.as_slice(),
        [AppEffect::CloseRepositoryTab(id)] if *id == second
    ));
}

#[test]
fn alt_number_activates_the_repository_tab_at_that_position() {
    let (tabs, first, second, third) = repository_tabs();
    let mut app = app_with_repository_tabs(&tabs);
    let now = Instant::now();

    for (number, expected) in [('1', first), ('2', second), ('3', third)] {
        let effects = app.handle_key(KeyEvent::new(KeyCode::Char(number), KeyModifiers::ALT), now);
        assert!(matches!(
            effects.as_slice(),
            [AppEffect::ActivateRepositoryTab(id)] if *id == expected
        ));
    }

    let effects = app.handle_key(KeyEvent::new(KeyCode::Char('4'), KeyModifiers::ALT), now);
    assert!(effects.is_empty());
}

#[test]
fn control_w_edits_an_open_text_modal_instead_of_closing_its_tab() {
    let (tabs, ..) = repository_tabs();
    let mut app = app_with_repository_tabs(&tabs);
    app.modal = Some(Modal::Commit {
        input: TextBuffer::new("keep this draft"),
        amend: false,
    });

    let effects = app.handle_key(
        KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL),
        Instant::now(),
    );

    assert!(effects.is_empty());
    assert!(matches!(
        app.modal,
        Some(Modal::Commit { input, .. }) if input.value == "keep this "
    ));
}

#[test]
fn clicking_a_repository_tab_activates_it_on_release() {
    let (tabs, _, second, _) = repository_tabs();
    let mut app = app_with_repository_tabs(&tabs);
    let now = Instant::now();

    let effects = app.handle_mouse(mouse(MouseEventKind::Down(MouseButton::Left), 14, 0), now);
    assert!(effects.is_empty());
    assert_eq!(
        app.repository_tab_drag,
        Some(RepositoryTabDrag {
            id: second,
            target: None,
        })
    );

    let effects = app.handle_mouse(mouse(MouseEventKind::Up(MouseButton::Left), 14, 0), now);
    assert!(matches!(
        effects.as_slice(),
        [AppEffect::ActivateRepositoryTab(id)] if *id == second
    ));
    assert_eq!(app.repository_tab_drag, None);
}

#[test]
fn overflow_controls_cycle_to_hidden_repository_tabs() {
    let (tabs, _, second, third) = repository_tabs();
    let mut app = app_with_repository_tabs(&tabs);
    app.geometry.repository_tab_previous = Rect::new(50, 0, 3, 1);
    app.geometry.repository_tab_next = Rect::new(53, 0, 3, 1);
    let now = Instant::now();

    let effects = app.handle_mouse(mouse(MouseEventKind::Down(MouseButton::Left), 51, 0), now);
    assert!(matches!(
        effects.as_slice(),
        [AppEffect::ActivateRepositoryTab(id)] if *id == third
    ));

    let effects = app.handle_mouse(mouse(MouseEventKind::Down(MouseButton::Left), 54, 0), now);
    assert!(matches!(
        effects.as_slice(),
        [AppEffect::ActivateRepositoryTab(id)] if *id == second
    ));
}

#[test]
fn dragging_a_repository_tab_reorders_without_activating_on_release() {
    let (tabs, first, _, third) = repository_tabs();
    let mut app = app_with_repository_tabs(&tabs);
    let now = Instant::now();

    assert!(
        app.handle_mouse(mouse(MouseEventKind::Down(MouseButton::Left), 2, 0), now)
            .is_empty()
    );
    let effects = app.handle_mouse(mouse(MouseEventKind::Drag(MouseButton::Left), 26, 0), now);
    assert!(effects.is_empty());
    assert_eq!(
        app.repository_tab_drag,
        Some(RepositoryTabDrag {
            id: first,
            target: Some(third),
        })
    );

    let effects = app.handle_mouse(mouse(MouseEventKind::Up(MouseButton::Left), 26, 0), now);
    assert!(matches!(
        effects.as_slice(),
        [AppEffect::ReorderRepositoryTab { source, target }]
            if *source == first && *target == third
    ));
    assert_eq!(app.repository_tab_drag, None);
}

#[test]
fn clicking_a_repository_tab_close_icon_closes_that_tab() {
    let (tabs, _, second, _) = repository_tabs();
    let mut app = app_with_repository_tabs(&tabs);

    let effects = app.handle_mouse(
        mouse(MouseEventKind::Down(MouseButton::Left), 21, 0),
        Instant::now(),
    );

    assert!(matches!(
        effects.as_slice(),
        [AppEffect::CloseRepositoryTab(id)] if *id == second
    ));
    assert_eq!(app.repository_tab_drag, None);
}

#[test]
fn right_click_menu_routes_close_others_and_dismisses_outside() {
    let (tabs, _, second, _) = repository_tabs();
    let mut app = app_with_repository_tabs(&tabs);
    let now = Instant::now();

    let effects = app.handle_mouse(mouse(MouseEventKind::Down(MouseButton::Right), 14, 0), now);
    assert!(effects.is_empty());
    assert_eq!(
        app.repository_tab_menu,
        Some(RepositoryTabMenu {
            id: second,
            column: 14,
            row: 0,
            selected: 0,
        })
    );

    app.geometry.repository_tab_menu_hits =
        vec![(Rect::new(14, 3, 20, 1), RepositoryTabAction::CloseOthers)];
    let effects = app.handle_mouse(mouse(MouseEventKind::Down(MouseButton::Left), 18, 3), now);
    assert!(matches!(
        effects.as_slice(),
        [AppEffect::CloseOtherRepositoryTabs(id)] if *id == second
    ));
    assert_eq!(app.repository_tab_menu, None);

    drop(app.handle_mouse(mouse(MouseEventKind::Down(MouseButton::Right), 14, 0), now));
    assert!(app.repository_tab_menu.is_some());
    let effects = app.handle_mouse(mouse(MouseEventKind::Down(MouseButton::Right), 70, 8), now);
    assert!(effects.is_empty());
    assert_eq!(app.repository_tab_menu, None);
}

#[test]
fn right_click_menu_opens_projects_and_routes_close_all() {
    let (tabs, _, second, _) = repository_tabs();
    let mut app = app_with_repository_tabs(&tabs);
    let now = Instant::now();

    drop(app.handle_mouse(mouse(MouseEventKind::Down(MouseButton::Right), 14, 0), now));
    let effects = app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE), now);
    assert!(matches!(
        app.modal,
        Some(Modal::Projects {
            mode: ProjectOpenMode::NewTab,
            ..
        })
    ));
    assert!(effects.iter().any(|effect| matches!(
        effect,
        AppEffect::Git(command)
            if matches!(command.as_ref(), WorkerCommand::LoadRecentProjects { .. })
    )));
    assert_eq!(app.repository_tab_menu, None);

    app.modal = None;
    drop(app.handle_mouse(mouse(MouseEventKind::Down(MouseButton::Right), 14, 0), now));
    let effects = app.handle_key(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE), now);
    assert!(effects.is_empty());
    assert_eq!(
        app.repository_tab_menu,
        Some(RepositoryTabMenu {
            id: second,
            column: 14,
            row: 0,
            selected: 3,
        })
    );
    let effects = app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE), now);
    assert!(matches!(
        effects.as_slice(),
        [AppEffect::CloseAllRepositoryTabs]
    ));
    assert_eq!(app.repository_tab_menu, None);
}

#[test]
fn repository_tab_commands_leave_each_app_view_state_independent() {
    let (mut tabs, first, second, _) = repository_tabs();
    let now = Instant::now();
    let mut first_app = app_with_repository_tabs(&tabs);
    first_app.view = View::PullRequests;
    first_app.content_scroll = 47;
    first_app.horizontal_scroll = 9;

    assert!(tabs.activate(second));
    let mut second_app = app_with_repository_tabs(&tabs);
    second_app.view = View::History;
    second_app.history_cursor = 6;
    second_app.content_scroll = 18;
    second_app.set_tab_active(false, now);

    let effects = first_app.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::CONTROL), now);
    assert!(matches!(
        effects.as_slice(),
        [AppEffect::ActivateRepositoryTab(id)] if *id == second
    ));
    assert_eq!(first_app.view, View::PullRequests);
    assert_eq!(first_app.content_scroll, 47);
    assert_eq!(first_app.horizontal_scroll, 9);
    assert_eq!(second_app.view, View::History);
    assert_eq!(second_app.history_cursor, 6);
    assert_eq!(second_app.content_scroll, 18);

    assert!(tabs.activate(first));
    first_app.set_repository_tabs(tabs.infos());
    assert_eq!(first_app.view, View::PullRequests);
    assert_eq!(first_app.content_scroll, 47);
}